Pharos 0.7.23
Modern Web Framework & Web App Hosting Service.
Loading...
Searching...
No Matches
native_json_runtime_ops.c
Go to the documentation of this file.
2
4#include "native_support.h"
5#include "native_fs.h"
6#include "native_app_config.h"
15
16#include <ctype.h>
17#include <stdarg.h>
18#include <stdio.h>
19#include <stdlib.h>
20#include <string.h>
21#include <strings.h>
22#include <time.h>
23#include <sqlite3.h>
24#ifndef _WIN32
25#include <dlfcn.h>
26#include <unistd.h>
27#endif
28
29static const char *json_option_value_runtime(int argc, char **argv, const char *name) {
30 int index;
31 if (argv == NULL || name == NULL) {
32 return NULL;
33 }
34 for (index = 0; index < argc - 1; index++) {
35 if (streq(argv[index], name)) {
36 return argv[index + 1];
37 }
38 }
39 return NULL;
40}
41
42static int emit_text_line_runtime(const char *text) {
43 if (text == NULL) {
44 return 69;
45 }
46 fputs(text, stdout);
47 fputc('\n', stdout);
48 return 0;
49}
50
51typedef int (*NativeJsonCommandHandlerFn)(int argc, char **argv);
52
59
61
62typedef struct {
63 char *text;
64 size_t length;
65 size_t capacity;
67
69 if (buffer == NULL) {
70 return;
71 }
72 free(buffer->text);
73 buffer->text = NULL;
74 buffer->length = 0;
75 buffer->capacity = 0;
76}
77
78static int text_buffer_reserve_runtime(TextBufferRuntime *buffer, size_t needed_length) {
79 char *next_text = NULL;
80 size_t next_capacity = 0;
81 if (buffer == NULL) {
82 return 0;
83 }
84 if (needed_length + 1 <= buffer->capacity) {
85 return 1;
86 }
87 next_capacity = buffer->capacity > 0 ? buffer->capacity : 256;
88 while (next_capacity <= needed_length) {
89 if (next_capacity > ((size_t)-1) / 2) {
90 return 0;
91 }
92 next_capacity *= 2;
93 }
94 next_text = (char *)realloc(buffer->text, next_capacity);
95 if (next_text == NULL) {
96 return 0;
97 }
98 buffer->text = next_text;
99 buffer->capacity = next_capacity;
100 if (buffer->length == 0) {
101 buffer->text[0] = '\0';
102 }
103 return 1;
104}
105
106static int text_buffer_append_text_runtime(TextBufferRuntime *buffer, const char *text) {
107 size_t text_length = 0;
108 if (buffer == NULL || text == NULL) {
109 return 0;
110 }
111 text_length = strlen(text);
112 if (!text_buffer_reserve_runtime(buffer, buffer->length + text_length)) {
113 return 0;
114 }
115 memcpy(buffer->text + buffer->length, text, text_length + 1);
116 buffer->length += text_length;
117 return 1;
118}
119
120static int text_buffer_append_char_runtime(TextBufferRuntime *buffer, char value) {
121 if (buffer == NULL) {
122 return 0;
123 }
124 if (!text_buffer_reserve_runtime(buffer, buffer->length + 1)) {
125 return 0;
126 }
127 buffer->text[buffer->length++] = value;
128 buffer->text[buffer->length] = '\0';
129 return 1;
130}
131
132static int text_buffer_append_format_runtime(TextBufferRuntime *buffer, const char *format, ...) {
133 va_list args;
134 va_list args_copy;
135 int needed = 0;
136 if (buffer == NULL || format == NULL) {
137 return 0;
138 }
139 va_start(args, format);
140 va_copy(args_copy, args);
141 needed = vsnprintf(NULL, 0, format, args_copy);
142 va_end(args_copy);
143 if (needed < 0 || !text_buffer_reserve_runtime(buffer, buffer->length + (size_t)needed)) {
144 va_end(args);
145 return 0;
146 }
147 vsnprintf(buffer->text + buffer->length, buffer->capacity - buffer->length, format, args);
148 va_end(args);
149 buffer->length += (size_t)needed;
150 return 1;
151}
152
154 char *text = NULL;
155 if (buffer == NULL) {
156 return NULL;
157 }
158 if (buffer->text == NULL) {
159 return strdup("");
160 }
161 text = buffer->text;
162 buffer->text = NULL;
163 buffer->length = 0;
164 buffer->capacity = 0;
165 return text;
166}
167
169 int argc,
170 char **argv,
171 const NativeJsonCommandDispatchEntry *entries,
172 size_t entry_count
173) {
174 size_t index = 0;
175 if (argc < 1 || argv == NULL || entries == NULL) {
176 return 78;
177 }
178 for (index = 0; index < entry_count; index++) {
179 const NativeJsonCommandDispatchEntry *entry = &entries[index];
180 int group_match = 0;
181 int command_match = 0;
182 if (entry->group_name == NULL || entry->handler == NULL) {
183 continue;
184 }
185 group_match = streq(argv[0], entry->group_name);
186 if (!group_match) {
187 continue;
188 }
189 if (entry->command_name == NULL) {
190 command_match = 1;
191 } else if (argc >= 2) {
192 command_match = streq(argv[1], entry->command_name);
193 }
194 if (!command_match) {
195 continue;
196 }
197 if (argc < entry->argv_offset) {
198 return 78;
199 }
200 return entry->handler(argc - entry->argv_offset, argv + entry->argv_offset);
201 }
202 return 78;
203}
204
205static int handle_bundle_manifest_slice_order_command(int argc, char **argv) {
206 const char *manifest_path = json_option_value_runtime(argc, argv, "--manifest");
207 yyjson_doc *doc = NULL;
208 yyjson_val *root = NULL;
209 yyjson_val *slices = NULL;
210 yyjson_val *slice_order = NULL;
211 yyjson_arr_iter iter;
212 yyjson_val *item = NULL;
213 int emitted = 0;
214 if (manifest_path == NULL || manifest_path[0] == '\0') {
215 return 64;
216 }
217 doc = native_json_doc_load_file(manifest_path);
218 if (doc == NULL) {
219 return 66;
220 }
221 root = yyjson_doc_get_root(doc);
222 slices = native_json_obj_get_array(root, "slices");
223 if (slices != NULL && yyjson_arr_iter_init(slices, &iter)) {
224 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
225 char *path = native_json_obj_get_string_dup(item, "path");
226 if (path != NULL) {
227 fputs(path, stdout);
228 fputc('\n', stdout);
229 emitted = 1;
230 }
231 free(path);
232 }
233 }
234 if (!emitted) {
235 slice_order = native_json_obj_get_array(root, "slice_order");
236 if (slice_order != NULL && yyjson_arr_iter_init(slice_order, &iter)) {
237 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
238 char *line = NULL;
239 if (!yyjson_is_str(item)) {
240 continue;
241 }
243 if (line != NULL) {
244 size_t len = strlen(line);
245 if (len >= 2 && line[0] == '"' && line[len - 1] == '"') {
246 line[len - 1] = '\0';
247 fputs(line + 1, stdout);
248 fputc('\n', stdout);
249 emitted = 1;
250 }
251 }
252 free(line);
253 }
254 }
255 }
257 return emitted ? 0 : 66;
258}
259
261static int json_value_truthy_runtime(yyjson_val *value);
262static int text_buffer_append_html_escaped_runtime(TextBufferRuntime *buffer, const char *text);
264static int digits_only_text_runtime(const char *text);
272static int template_reference_contains_unsafe_char_runtime(const char *text);
273static int template_reference_has_scheme_runtime(const char *text);
274static int template_reference_has_allowed_url_scheme_runtime(const char *text);
275static int template_path_reference_allowed_runtime(const char *text);
276static int template_url_reference_allowed_runtime(const char *text);
278static TemplatePlaceholderKindRuntime template_placeholder_kind_runtime(const char *placeholder_name, const char **param_name);
279static char *render_template_text_with_default_kind_dup_runtime(const char *template_text, yyjson_val *params_root, TemplatePlaceholderKindRuntime default_kind);
280static char *render_business_fragment_dup_runtime(const char *app_root, const char *fragment_name, yyjson_val *params_root);
281static char *render_business_fragment_from_doc_dup_runtime(const char *app_root, const char *fragment_name, yyjson_mut_doc *doc);
282static char *render_service_visual_html_dup_runtime(const char *app_root, const char *image_url, const char *service_name);
283static char *render_service_sort_order_options_html_dup_runtime(yyjson_val *root, long business_id, long selected_order, int include_append, const char *app_root);
284static char *render_service_booking_options_editor_html_dup_runtime(yyjson_val *root, long service_id, const char *app_root);
285static int emit_service_book_form_page_json_runtime(yyjson_val *root, const char *slug, long service_id, const char *app_root, const char *error_message);
286static char *render_technician_checkboxes_html_dup_runtime(yyjson_val *root, long business_id, long service_id, const char *empty_message, int select_all, const char *app_root);
287static int parse_hhmm_minutes_runtime(const char *value, int *minutes_out);
288static int parse_ymd_runtime(const char *date_value, int *year_out, int *month_out, int *day_out);
289static int time_ranges_overlap_runtime(int start_a_minutes, int duration_a, int start_b_minutes, int duration_b);
290static int business_open_for_slot_runtime(yyjson_val *root, long business_id, const char *date_value, const char *time_value, long duration_minutes);
291static int technician_booked_for_slot_runtime(yyjson_val *root, long technician_id, const char *date_value, const char *time_value, long duration_minutes);
292static long assign_technician_for_slot_runtime(yyjson_val *root, long business_id, long service_id, const char *date_value, const char *time_value);
293static int days_in_month_runtime(int year, int month);
294static const char *month_name_runtime(int month);
295static void month_shift_runtime(int year, int month, int shift, int *out_year, int *out_month);
296static int weekday_monday_index_runtime(int year, int month, int day);
297static const char *day_abbrev_for_ymd_runtime(int year, int month, int day);
298static int append_date_runtime(TextBufferRuntime *buffer, int year, int month, int day);
299static char *url_decode_dup_runtime(const char *text);
300static char *query_value_decoded_dup_runtime(const char *raw_query, const char *key);
301static char *resolve_command_path_runtime(const char *command);
302static int command_exists_in_path_runtime(const char *command);
303static int run_sqlite_command_runtime(const char *sqlite_path, const char *sql_text, char **output_out);
304static char *sqlite_query_value_runtime(const char *sqlite_path, const char *sql_text);
305static int append_escaped_sql_char_runtime(TextBufferRuntime *buffer, char value);
306static char *sql_literal_escape_dup_runtime(const char *value);
307static int write_temp_text_file_runtime(const char *text, char **path_out);
308static char *capture_checksum_output_runtime(const char *command, const char *arg1, const char *arg2, const char *file_path, int *code_out);
309static char *checksum_string_runtime(const char *value);
310static char *cksum_signature_runtime(const char *value);
311static char *dynamic_sqlite_path_dup_runtime(const char *app_root, const char *runtime_conf_path, const char *app_id);
312static int sqlite_apply_file_list_runtime(const char *sqlite_path, const char *app_root, yyjson_val *list);
313static char *migration_expected_checksum_dup_runtime(const char *app_root, yyjson_val *migration_record, const char *backend);
314static yyjson_val *migration_backend_record_runtime(yyjson_val *root, const char *backend_value);
315static char *ucal_runtime_state_export_sqlite_path_dup_runtime(const char *app_root);
316static char *ucal_runtime_state_import_sqlite_path_dup_runtime(const char *app_root);
317static char *ucal_runtime_state_export_postgresql_path_dup_runtime(const char *app_root);
318static char *ucal_runtime_state_import_postgresql_path_dup_runtime(const char *app_root);
319static int run_postgresql_sql_capture_runtime(const DynamicPostgresqlRuntimeConfig *config, const char *sql_text, int at_output, char **output_out);
320static char *dynamic_relational_runtime_export_json_dup_runtime(const char *app_root, const char *runtime_conf_path, const char *app_id);
321static char *dynamic_relational_runtime_audit_count_dup_runtime(const char *app_root, const char *runtime_conf_path, const char *app_id);
322static int dynamic_relational_runtime_import_state_runtime(const char *app_root, const char *runtime_conf_path, const char *app_id, const char *state_path, const char *expected_audit_count);
323static int handle_template_apply_base_path_command(int argc, char **argv);
324static int handle_template_declared_page_contract_command(int argc, char **argv);
325static int handle_template_declared_fragment_path_command(int argc, char **argv);
326static int handle_template_layout_page_html_command(int argc, char **argv);
327static int handle_template_layout_page_json_command(int argc, char **argv);
328static int handle_template_login_page_json_command(int argc, char **argv);
329static int handle_template_register_page_json_command(int argc, char **argv);
330static int handle_template_password_reset_request_page_json_command(int argc, char **argv);
331static int handle_template_password_reset_sent_page_json_command(int argc, char **argv);
332static int handle_template_password_reset_complete_page_json_command(int argc, char **argv);
333static int handle_template_invite_created_page_json_command(int argc, char **argv);
334static int handle_template_invite_accept_page_json_command(int argc, char **argv);
335static int handle_generated_surface_manage_page_json_command(int argc, char **argv);
336static int handle_generated_surface_edit_page_json_command(int argc, char **argv);
337
338yyjson_val *load_root_from_text_runtime(const char *text, yyjson_doc **doc_out) {
339 yyjson_doc *doc = NULL;
340 yyjson_val *root = NULL;
341 if (doc_out != NULL) {
342 *doc_out = NULL;
343 }
344 if (text == NULL || text[0] == '\0') {
345 return NULL;
346 }
347 doc = native_json_doc_load_text(text);
348 if (doc == NULL) {
349 return NULL;
350 }
351 root = yyjson_doc_get_root(doc);
352 if (doc_out != NULL) {
353 *doc_out = doc;
354 } else {
356 }
357 return root;
358}
359
361 char *text = NULL;
362 int code = 69;
363 if (value == NULL) {
364 return 69;
365 }
367 if (text != NULL) {
368 fputs(text, stdout);
369 code = 0;
370 }
371 free(text);
372 return code;
373}
374
376 char *text = NULL;
377 if (doc == NULL) {
378 return 69;
379 }
380 text = yyjson_mut_write(doc, YYJSON_WRITE_NOFLAG, NULL);
381 if (text == NULL) {
382 return 69;
383 }
384 fputs(text, stdout);
385 free(text);
386 return 0;
387}
388
389static int write_mutable_json_doc_runtime(yyjson_mut_doc *doc, const char *path) {
390 char *text = NULL;
391 int code = 69;
392 if (doc == NULL || path == NULL || path[0] == '\0') {
393 return 64;
394 }
395 text = yyjson_mut_write(doc, YYJSON_WRITE_NOFLAG, NULL);
396 if (text == NULL) {
397 return 69;
398 }
399 code = write_text_file(path, text);
400 free(text);
401 return code;
402}
403
404/**
405 * @brief Reads one file into an owned binary buffer and appends a trailing NUL byte for text helpers.
406 * @param path File path to load.
407 * @param out_size Receives the byte count excluding the trailing NUL.
408 * @return Newly allocated buffer, or NULL on failure.
409 */
410unsigned char *read_file_bytes_dup_runtime(const char *path, size_t *out_size) {
411 FILE *file = NULL;
412 long file_size = 0;
413 size_t bytes_read = 0;
414 unsigned char *buffer = NULL;
415 if (out_size != NULL) {
416 *out_size = 0;
417 }
418 if (path == NULL || path[0] == '\0') {
419 return NULL;
420 }
421 file = fopen(path, "rb");
422 if (file == NULL) {
423 return NULL;
424 }
425 if (fseek(file, 0, SEEK_END) != 0) {
426 fclose(file);
427 return NULL;
428 }
429 file_size = ftell(file);
430 if (file_size < 0 || fseek(file, 0, SEEK_SET) != 0) {
431 fclose(file);
432 return NULL;
433 }
434 buffer = (unsigned char *)malloc((size_t)file_size + 1);
435 if (buffer == NULL) {
436 fclose(file);
437 return NULL;
438 }
439 if (file_size > 0) {
440 bytes_read = fread(buffer, 1, (size_t)file_size, file);
441 if (bytes_read != (size_t)file_size) {
442 free(buffer);
443 fclose(file);
444 return NULL;
445 }
446 }
447 buffer[(size_t)file_size] = '\0';
448 fclose(file);
449 if (out_size != NULL) {
450 *out_size = (size_t)file_size;
451 }
452 return buffer;
453}
454
455/**
456 * @brief Writes one binary byte range to a file path, creating parent directories as needed.
457 * @param path Destination path.
458 * @param data Bytes to write.
459 * @param length Number of bytes to write.
460 * @return 0 on success, or a non-zero status code on failure.
461 */
462int write_binary_file_runtime(const char *path, const unsigned char *data, size_t length) {
463 FILE *file = NULL;
464 char *dir = NULL;
465 if (path == NULL || path[0] == '\0' || data == NULL) {
466 return 64;
467 }
468 dir = path_dirname(path);
469 if (dir == NULL || ensure_directory(dir) != 0) {
470 free(dir);
471 return 69;
472 }
473 free(dir);
474 file = fopen(path, "wb");
475 if (file == NULL) {
476 return 69;
477 }
478 if (length > 0 && fwrite(data, 1, length, file) != length) {
479 fclose(file);
480 return 69;
481 }
482 if (fclose(file) != 0) {
483 return 69;
484 }
485 return 0;
486}
487
488/**
489 * @brief Finds the first binary needle occurrence within a byte range.
490 * @param haystack Bytes to scan.
491 * @param haystack_len Available byte count.
492 * @param needle Needle bytes.
493 * @param needle_len Needle byte count.
494 * @return Pointer to the first match, or NULL when absent.
495 */
496static const unsigned char *memmem_runtime(const unsigned char *haystack, size_t haystack_len, const unsigned char *needle, size_t needle_len) {
497 size_t index = 0;
498 if (haystack == NULL || needle == NULL || needle_len == 0 || haystack_len < needle_len) {
499 return NULL;
500 }
501 for (index = 0; index + needle_len <= haystack_len; index++) {
502 if (memcmp(haystack + index, needle, needle_len) == 0) {
503 return haystack + index;
504 }
505 }
506 return NULL;
507}
508
509/**
510 * @brief Extracts the multipart boundary token from a content type value.
511 * @param content_type Multipart content type header value.
512 * @return Newly allocated boundary token, or NULL when the header has no boundary.
513 */
514static char *multipart_boundary_dup_runtime(const char *content_type) {
515 const char *boundary_start = NULL;
516 const char *boundary_end = NULL;
517 if (content_type == NULL) {
518 return NULL;
519 }
520 boundary_start = strstr(content_type, "boundary=");
521 if (boundary_start == NULL) {
522 return NULL;
523 }
524 boundary_start += 9;
525 if (*boundary_start == '"') {
526 boundary_start++;
527 boundary_end = strchr(boundary_start, '"');
528 } else {
529 boundary_end = boundary_start;
530 while (*boundary_end != '\0' && *boundary_end != ';' && !isspace((unsigned char)*boundary_end)) {
531 boundary_end++;
532 }
533 }
534 if (boundary_end == NULL || boundary_end <= boundary_start) {
535 return NULL;
536 }
537 return dup_range(boundary_start, (size_t)(boundary_end - boundary_start));
538}
539
540/**
541 * @brief Extracts one quoted Content-Disposition parameter from multipart headers.
542 * @param headers_text NUL-terminated part headers.
543 * @param key Parameter name such as name or filename.
544 * @return Newly allocated parameter value, or NULL when absent.
545 */
546static char *multipart_header_param_dup_runtime(const char *headers_text, const char *key) {
547 char pattern[64];
548 const char *match = NULL;
549 const char *value_start = NULL;
550 const char *value_end = NULL;
551 if (headers_text == NULL || key == NULL || key[0] == '\0') {
552 return NULL;
553 }
554 snprintf(pattern, sizeof(pattern), "%s=\"", key);
555 match = strstr(headers_text, pattern);
556 if (match == NULL) {
557 return NULL;
558 }
559 value_start = match + strlen(pattern);
560 value_end = strchr(value_start, '"');
561 if (value_end == NULL || value_end < value_start) {
562 return NULL;
563 }
564 return dup_range(value_start, (size_t)(value_end - value_start));
565}
566
567/**
568 * @brief Finds one multipart part by field name and optionally returns its filename and content range.
569 * @param body Multipart request body bytes.
570 * @param body_len Multipart request body length.
571 * @param content_type Multipart content type header value.
572 * @param field_name Target field name.
573 * @param out_text Receives owned text content when requested.
574 * @param out_filename Receives owned uploaded filename when requested.
575 * @param out_content_start Receives the part content start within body when requested.
576 * @param out_content_len Receives the part content length when requested.
577 * @return 1 when a matching part is found, otherwise 0.
578 */
580 const unsigned char *body,
581 size_t body_len,
582 const char *content_type,
583 const char *field_name,
584 char **out_text,
585 char **out_filename,
586 const unsigned char **out_content_start,
587 size_t *out_content_len
588) {
589 char *boundary = NULL;
590 char *delimiter = NULL;
591 char *delimiter_crlf = NULL;
592 char *delimiter_lf = NULL;
593 const unsigned char *cursor = NULL;
594 size_t delimiter_len = 0;
595 int found = 0;
596 if (out_text != NULL) *out_text = NULL;
597 if (out_filename != NULL) *out_filename = NULL;
598 if (out_content_start != NULL) *out_content_start = NULL;
599 if (out_content_len != NULL) *out_content_len = 0;
600 if (body == NULL || body_len == 0 || content_type == NULL || field_name == NULL || field_name[0] == '\0') {
601 return 0;
602 }
603 boundary = multipart_boundary_dup_runtime(content_type);
604 if (boundary == NULL || boundary[0] == '\0') {
605 free(boundary);
606 return 0;
607 }
608 delimiter_len = strlen(boundary) + 2;
609 delimiter = (char *)malloc(delimiter_len + 1);
610 delimiter_crlf = (char *)malloc(delimiter_len + 3);
611 delimiter_lf = (char *)malloc(delimiter_len + 2);
612 if (delimiter == NULL || delimiter_crlf == NULL || delimiter_lf == NULL) {
613 free(boundary); free(delimiter); free(delimiter_crlf); free(delimiter_lf);
614 return 0;
615 }
616 snprintf(delimiter, delimiter_len + 1, "--%s", boundary);
617 snprintf(delimiter_crlf, delimiter_len + 3, "\r\n--%s", boundary);
618 snprintf(delimiter_lf, delimiter_len + 2, "\n--%s", boundary);
619 cursor = memmem_runtime(body, body_len, (const unsigned char *)delimiter, delimiter_len);
620 while (cursor != NULL && (size_t)(cursor - body) + delimiter_len <= body_len) {
621 const unsigned char *part_cursor = cursor + delimiter_len;
622 const unsigned char *header_end = NULL;
623 const unsigned char *content_start = NULL;
624 const unsigned char *next_crlf = NULL;
625 const unsigned char *next_lf = NULL;
626 const unsigned char *next_marker = NULL;
627 size_t header_sep_len = 0;
628 size_t headers_len = 0;
629 size_t content_len = 0;
630 char *headers_text = NULL;
631 char *part_name = NULL;
632 char *filename = NULL;
633 char *text_value = NULL;
634 if ((size_t)(part_cursor - body) + 2 <= body_len && part_cursor[0] == '-' && part_cursor[1] == '-') {
635 break;
636 }
637 if ((size_t)(part_cursor - body) + 2 <= body_len && part_cursor[0] == '\r' && part_cursor[1] == '\n') {
638 part_cursor += 2;
639 } else if ((size_t)(part_cursor - body) + 1 <= body_len && part_cursor[0] == '\n') {
640 part_cursor += 1;
641 }
642 next_crlf = memmem_runtime(part_cursor, body_len - (size_t)(part_cursor - body), (const unsigned char *)"\r\n\r\n", 4);
643 next_lf = memmem_runtime(part_cursor, body_len - (size_t)(part_cursor - body), (const unsigned char *)"\n\n", 2);
644 if (next_crlf != NULL && (next_lf == NULL || next_crlf <= next_lf)) {
645 header_end = next_crlf;
646 header_sep_len = 4;
647 } else if (next_lf != NULL) {
648 header_end = next_lf;
649 header_sep_len = 2;
650 } else {
651 break;
652 }
653 headers_len = (size_t)(header_end - part_cursor);
654 headers_text = dup_range((const char *)part_cursor, headers_len);
655 part_name = multipart_header_param_dup_runtime(headers_text, "name");
656 filename = multipart_header_param_dup_runtime(headers_text, "filename");
657 content_start = header_end + header_sep_len;
658 next_crlf = memmem_runtime(content_start, body_len - (size_t)(content_start - body), (const unsigned char *)delimiter_crlf, delimiter_len + 2);
659 next_lf = memmem_runtime(content_start, body_len - (size_t)(content_start - body), (const unsigned char *)delimiter_lf, delimiter_len + 1);
660 if (next_crlf != NULL && (next_lf == NULL || next_crlf <= next_lf)) {
661 next_marker = next_crlf;
662 } else if (next_lf != NULL) {
663 next_marker = next_lf;
664 } else {
665 next_marker = body + body_len;
666 }
667 content_len = (size_t)(next_marker - content_start);
668 if (part_name != NULL && streq(part_name, field_name)) {
669 if (out_text != NULL) {
670 text_value = dup_range((const char *)content_start, content_len);
671 *out_text = text_value;
672 }
673 if (out_filename != NULL && filename != NULL) {
674 *out_filename = strdup(filename);
675 }
676 if (out_content_start != NULL) {
677 *out_content_start = content_start;
678 }
679 if (out_content_len != NULL) {
680 *out_content_len = content_len;
681 }
682 found = 1;
683 free(headers_text);
684 free(part_name);
685 free(filename);
686 break;
687 }
688 free(headers_text);
689 free(part_name);
690 free(filename);
691 if (next_marker >= body + body_len) {
692 break;
693 }
694 cursor = next_marker + (next_marker == next_crlf ? 2 : 1);
695 cursor = memmem_runtime(cursor, body_len - (size_t)(cursor - body), (const unsigned char *)delimiter, delimiter_len);
696 }
697 free(boundary);
698 free(delimiter);
699 free(delimiter_crlf);
700 free(delimiter_lf);
701 return found;
702}
703
704/**
705 * @brief Resolves one request form field from urlencoded or multipart request inputs.
706 * @param content_type Request content type header value.
707 * @param form_body Inline request body text when available.
708 * @param body_path Optional request body file path.
709 * @param field_name Field name to extract.
710 * @return Newly allocated field value, or NULL when absent.
711 */
712static char *request_form_field_value_dup_runtime(const char *content_type, const char *form_body, const char *body_path, const char *field_name) {
713 char *value = NULL;
714 if (field_name == NULL || field_name[0] == '\0') {
715 return NULL;
716 }
717 if (content_type != NULL && strstr(content_type, "multipart/form-data") != NULL && body_path != NULL && body_path[0] != '\0') {
718 unsigned char *body_bytes = NULL;
719 size_t body_len = 0;
720 body_bytes = read_file_bytes_dup_runtime(body_path, &body_len);
721 if (body_bytes != NULL) {
722 multipart_find_part_runtime(body_bytes, body_len, content_type, field_name, &value, NULL, NULL, NULL);
723 }
724 free(body_bytes);
725 return value;
726 }
727 {
728 char *body_text = NULL;
729 const char *raw_form = form_body != NULL ? form_body : "";
730 if (body_path != NULL && body_path[0] != '\0' && path_exists(body_path)) {
731 body_text = read_file_text(body_path);
732 raw_form = body_text != NULL ? body_text : raw_form;
733 }
734 value = query_value_decoded_dup_runtime(raw_form, field_name);
735 free(body_text);
736 }
737 return value;
738}
739
740/**
741 * @brief Returns one request form field value from urlencoded or multipart request inputs.
742 * @param argc Command argument count.
743 * @param argv Command argument vector.
744 * @return Native status code.
745 */
746static int handle_request_form_field_command(int argc, char **argv) {
747 const char *content_type = json_option_value_runtime(argc, argv, "--content-type");
748 const char *form_body = json_option_value_runtime(argc, argv, "--form-body");
749 const char *body_path = json_option_value_runtime(argc, argv, "--request-body-path");
750 const char *field_name = json_option_value_runtime(argc, argv, "--field");
751 char *value = NULL;
752 if (field_name == NULL || field_name[0] == '\0') {
753 return 64;
754 }
755 value = request_form_field_value_dup_runtime(content_type, form_body, body_path, field_name);
756 if (value != NULL) {
757 fputs(value, stdout);
758 }
759 free(value);
760 return 0;
761}
762
763/**
764 * @brief Extracts a cookie value from a Cookie header string.
765 *
766 * Registered as: request cookie-value --cookie HEADER --name NAME
767 */
768static int handle_request_cookie_value_command(int argc, char **argv) {
769 const char *cookie_header = json_option_value_runtime(argc, argv, "--cookie");
770 const char *name = json_option_value_runtime(argc, argv, "--name");
771 char *value = NULL;
772 if (name == NULL || name[0] == '\0') {
773 return 64;
774 }
775 value = native_dynamic_cookie_value(cookie_header, name);
776 if (value != NULL) {
777 fputs(value, stdout);
778 }
779 free(value);
780 return 0;
781}
782
783/**
784 * @brief Extracts a value from a URL-encoded form body.
785 *
786 * Registered as: request form-value --form-body BODY --key KEY
787 */
788static int handle_request_form_value_command(int argc, char **argv) {
789 const char *form_body = json_option_value_runtime(argc, argv, "--form-body");
790 const char *key = json_option_value_runtime(argc, argv, "--key");
791 char *value = NULL;
792 if (key == NULL || key[0] == '\0') {
793 return 64;
794 }
795 value = native_dynamic_form_value(form_body, key);
796 if (value != NULL) {
797 fputs(value, stdout);
798 }
799 free(value);
800 return 0;
801}
802
803/**
804 * @brief Extracts a parameter value from a query string.
805 *
806 * Registered as: request query-value --query QUERY --key KEY
807 */
808static int handle_request_query_value_command(int argc, char **argv) {
809 const char *query = json_option_value_runtime(argc, argv, "--query");
810 const char *key = json_option_value_runtime(argc, argv, "--key");
811 char *value = NULL;
812 if (key == NULL || key[0] == '\0') {
813 return 64;
814 }
815 value = native_dynamic_query_value(query, key);
816 if (value != NULL) {
817 fputs(value, stdout);
818 }
819 free(value);
820 return 0;
821}
822
823/**
824 * @brief Extracts the uploaded filename from a multipart file part.
825 *
826 * Registered as: dynamic multipart-filename --content-type CT --body-path PATH --field NAME
827 */
828static int handle_dynamic_multipart_filename_command(int argc, char **argv) {
829 const char *content_type = json_option_value_runtime(argc, argv, "--content-type");
830 const char *body_path = json_option_value_runtime(argc, argv, "--body-path");
831 const char *field_name = json_option_value_runtime(argc, argv, "--field");
832 unsigned char *body_bytes = NULL;
833 size_t body_len = 0;
834 char *filename = NULL;
835 if (field_name == NULL || field_name[0] == '\0' || content_type == NULL || body_path == NULL) {
836 return 64;
837 }
838 body_bytes = read_file_bytes_dup_runtime(body_path, &body_len);
839 if (body_bytes == NULL) {
840 return 1;
841 }
842 filename = native_dynamic_multipart_filename(body_bytes, body_len, content_type, field_name);
843 if (filename != NULL) {
844 fputs(filename, stdout);
845 }
846 free(filename);
847 free(body_bytes);
848 return 0;
849}
850
851/**
852 * @brief Extracts a text value from a multipart form field.
853 *
854 * Registered as: dynamic multipart-extract-text --content-type CT --body-path PATH --field NAME
855 */
856static int handle_dynamic_multipart_extract_text_command(int argc, char **argv) {
857 const char *content_type = json_option_value_runtime(argc, argv, "--content-type");
858 const char *body_path = json_option_value_runtime(argc, argv, "--body-path");
859 const char *field_name = json_option_value_runtime(argc, argv, "--field");
860 unsigned char *body_bytes = NULL;
861 size_t body_len = 0;
862 char *text = NULL;
863 if (field_name == NULL || field_name[0] == '\0' || content_type == NULL || body_path == NULL) {
864 return 64;
865 }
866 body_bytes = read_file_bytes_dup_runtime(body_path, &body_len);
867 if (body_bytes == NULL) {
868 return 1;
869 }
870 text = native_dynamic_multipart_extract_text(body_bytes, body_len, content_type, field_name);
871 if (text != NULL) {
872 fputs(text, stdout);
873 }
874 free(text);
875 free(body_bytes);
876 return 0;
877}
878
879/**
880 * @brief Extracts a file from a multipart upload and writes it to disk.
881 *
882 * Registered as: dynamic multipart-extract-file --content-type CT --body-path PATH --field NAME --output PATH
883 */
884static int handle_dynamic_multipart_extract_file_command(int argc, char **argv) {
885 const char *content_type = json_option_value_runtime(argc, argv, "--content-type");
886 const char *body_path = json_option_value_runtime(argc, argv, "--body-path");
887 const char *field_name = json_option_value_runtime(argc, argv, "--field");
888 const char *output_path = json_option_value_runtime(argc, argv, "--output");
889 unsigned char *body_bytes = NULL;
890 size_t body_len = 0;
891 int code;
892 if (field_name == NULL || field_name[0] == '\0' || content_type == NULL || body_path == NULL || output_path == NULL) {
893 return 64;
894 }
895 body_bytes = read_file_bytes_dup_runtime(body_path, &body_len);
896 if (body_bytes == NULL) {
897 return 1;
898 }
899 code = native_dynamic_multipart_extract_file(body_bytes, body_len, content_type, field_name, output_path);
900 free(body_bytes);
901 return code;
902}
903
904/**
905 * @brief Extracts booking option rows directly from repeated request form fields.
906 * @param argc Command argument count.
907 * @param argv Command argument vector.
908 * @return Native status code.
909 */
911 const char *content_type = json_option_value_runtime(argc, argv, "--content-type");
912 const char *form_body = json_option_value_runtime(argc, argv, "--form-body");
913 const char *body_path = json_option_value_runtime(argc, argv, "--request-body-path");
914 char *count_text = request_form_field_value_dup_runtime(content_type, form_body, body_path, "booking_option_count");
915 char *raw_lines = NULL;
916 yyjson_mut_doc *doc = NULL;
917 yyjson_mut_val *array = NULL;
918 long count_value = 0;
919 long index = 1;
920 int code = 69;
921 if (count_text != NULL && digits_only_text_runtime(count_text)) {
922 count_value = strtol(count_text, NULL, 10);
923 }
924 free(count_text);
925 if (count_value > 0) {
926 TextBufferRuntime buffer = {0};
927 for (index = 1; index <= count_value; index++) {
928 char field_name[64];
929 char *label_value = NULL;
930 char *duration_value = NULL;
931 char *cost_value = NULL;
932 snprintf(field_name, sizeof(field_name), "booking_option_label_%ld", index);
933 label_value = request_form_field_value_dup_runtime(content_type, form_body, body_path, field_name);
934 snprintf(field_name, sizeof(field_name), "booking_option_duration_%ld", index);
935 duration_value = request_form_field_value_dup_runtime(content_type, form_body, body_path, field_name);
936 snprintf(field_name, sizeof(field_name), "booking_option_cost_%ld", index);
937 cost_value = request_form_field_value_dup_runtime(content_type, form_body, body_path, field_name);
938 if ((label_value != NULL && label_value[0] != '\0') || (duration_value != NULL && duration_value[0] != '\0') || (cost_value != NULL && cost_value[0] != '\0')) {
939 if ((buffer.length > 0 && !text_buffer_append_text_runtime(&buffer, "\n"))
940 || !text_buffer_append_text_runtime(&buffer, label_value != NULL ? label_value : "")
941 || !text_buffer_append_text_runtime(&buffer, "|")
942 || !text_buffer_append_text_runtime(&buffer, duration_value != NULL ? duration_value : "")
943 || !text_buffer_append_text_runtime(&buffer, "|")
944 || !text_buffer_append_text_runtime(&buffer, cost_value != NULL ? cost_value : "")) {
945 free(label_value);
946 free(duration_value);
947 free(cost_value);
949 return 69;
950 }
951 }
952 free(label_value);
953 free(duration_value);
954 free(cost_value);
955 }
956 raw_lines = text_buffer_take_runtime(&buffer);
957 }
958 if (raw_lines == NULL || raw_lines[0] == '\0') {
959 free(raw_lines);
960 raw_lines = request_form_field_value_dup_runtime(content_type, form_body, body_path, "booking_options");
961 }
962 if (raw_lines == NULL || raw_lines[0] == '\0') {
963 fputs("[]", stdout);
964 free(raw_lines);
965 return 0;
966 }
967 doc = yyjson_mut_doc_new(NULL);
968 array = doc != NULL ? yyjson_mut_arr(doc) : NULL;
969 if (doc == NULL || array == NULL) {
971 free(raw_lines);
972 return 69;
973 }
974 yyjson_mut_doc_set_root(doc, array);
975 {
976 char *copy = raw_lines;
977 char *line = NULL;
978 char *saveptr = NULL;
979 while ((line = strtok_r(copy, "\n", &saveptr)) != NULL) {
980 char *parts[3] = { NULL, NULL, NULL };
981 char *cursor = line;
982 int part_index = 0;
983 copy = NULL;
984 while (part_index < 3) {
985 parts[part_index++] = cursor;
986 cursor = strchr(cursor, '|');
987 if (cursor == NULL) {
988 break;
989 }
990 *cursor = '\0';
991 cursor++;
992 }
993 if ((parts[0] != NULL && parts[0][0] != '\0') || (parts[1] != NULL && parts[1][0] != '\0') || (parts[2] != NULL && parts[2][0] != '\0')) {
994 yyjson_mut_val *obj = yyjson_mut_obj(doc);
995 long duration = parts[1] != NULL && digits_only_text_runtime(parts[1]) ? strtol(parts[1], NULL, 10) : 0;
996 yyjson_mut_obj_add_strcpy(doc, obj, "label", parts[0] != NULL ? parts[0] : "");
997 if (duration > 0) {
998 yyjson_mut_obj_add_int(doc, obj, "duration_minutes", duration);
999 } else {
1000 yyjson_mut_obj_add_int(doc, obj, "duration_minutes", 0);
1001 }
1002 yyjson_mut_obj_add_strcpy(doc, obj, "cost", parts[2] != NULL ? parts[2] : "");
1003 yyjson_mut_arr_append(array, obj);
1004 }
1005 }
1006 }
1009 free(raw_lines);
1010 return code;
1011}
1012
1013static int handle_bundle_manifest_layer_for_slice_command(int argc, char **argv) {
1014 const char *manifest_path = json_option_value_runtime(argc, argv, "--manifest");
1015 const char *slice_name = json_option_value_runtime(argc, argv, "--slice");
1016 yyjson_doc *doc = NULL;
1017 yyjson_val *root = NULL;
1018 yyjson_val *slices = NULL;
1019 yyjson_arr_iter iter;
1020 yyjson_val *item = NULL;
1021 int code = 0;
1022 if (manifest_path == NULL || manifest_path[0] == '\0' || slice_name == NULL || slice_name[0] == '\0') {
1023 return 64;
1024 }
1025 doc = native_json_doc_load_file(manifest_path);
1026 if (doc == NULL) {
1027 return 66;
1028 }
1029 root = yyjson_doc_get_root(doc);
1030 slices = native_json_obj_get_array(root, "slices");
1031 if (slices == NULL || !yyjson_arr_iter_init(slices, &iter)) {
1033 return 66;
1034 }
1035 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
1036 char *path = native_json_obj_get_string_dup(item, "path");
1037 if (path != NULL && streq(path, slice_name)) {
1038 char *layer = native_json_obj_get_string_dup(item, "materialize_from_runtime_layer");
1039 free(path);
1040 if (layer != NULL) {
1041 code = emit_text_line_runtime(layer);
1042 free(layer);
1044 return code;
1045 }
1047 return 0;
1048 }
1049 free(path);
1050 }
1052 return 0;
1053}
1054
1055static int handle_request_compact_json_command(int argc, char **argv) {
1056 const char *text = json_option_value_runtime(argc, argv, "--text");
1057 yyjson_doc *doc = NULL;
1058 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1059 int code = 0;
1060 if (root == NULL) {
1061 return 64;
1062 }
1065 return code;
1066}
1067
1068static int handle_request_compact_object_command(int argc, char **argv) {
1069 const char *text = json_option_value_runtime(argc, argv, "--text");
1070 yyjson_doc *doc = NULL;
1071 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1072 int code = 0;
1073 if (root == NULL || !yyjson_is_obj(root)) {
1074 fputs("{}", stdout);
1075 return 0;
1076 }
1079 return code;
1080}
1081
1082static int handle_request_compact_object_strict_command(int argc, char **argv) {
1083 const char *text = json_option_value_runtime(argc, argv, "--text");
1084 yyjson_doc *doc = NULL;
1085 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1086 int code = 0;
1087 if (root == NULL || !yyjson_is_obj(root)) {
1089 return 64;
1090 }
1093 return code;
1094}
1095
1096static int handle_request_field_command(int argc, char **argv) {
1097 const char *text = json_option_value_runtime(argc, argv, "--text");
1098 const char *field = json_option_value_runtime(argc, argv, "--field");
1099 yyjson_doc *doc = NULL;
1100 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1101 yyjson_val *value = NULL;
1102 char *copy = NULL;
1103 if (field == NULL || field[0] == '\0') {
1104 return 64;
1105 }
1106 if (root == NULL || !yyjson_is_obj(root)) {
1107 return 0;
1108 }
1109 value = native_json_obj_get(root, field);
1110 if (yyjson_is_str(value)) {
1111 copy = native_json_obj_get_string_dup(root, field);
1112 if (copy != NULL) {
1113 fputs(copy, stdout);
1114 }
1115 free(copy);
1116 }
1118 return 0;
1119}
1120
1121static int handle_manifest_fixture_path_raw_command(int argc, char **argv) {
1122 const char *manifest_path = json_option_value_runtime(argc, argv, "--manifest");
1123 const char *key = json_option_value_runtime(argc, argv, "--key");
1124 yyjson_doc *doc = NULL;
1125 yyjson_val *root = NULL;
1126 yyjson_val *value = NULL;
1127 char *copy = NULL;
1128 int code = 0;
1129 if (manifest_path == NULL || key == NULL || key[0] == '\0') {
1130 return 64;
1131 }
1132 doc = native_json_doc_load_file(manifest_path);
1133 if (doc == NULL) {
1134 return 66;
1135 }
1136 root = yyjson_doc_get_root(doc);
1137 value = native_json_obj_get(root, key);
1138 if (value == NULL) {
1139 yyjson_val *fixtures = native_json_obj_get(root, "fixtures");
1140 if (fixtures != NULL) {
1141 value = native_json_obj_get(fixtures, key);
1142 }
1143 }
1144 if (value == NULL) {
1145 yyjson_val *database = native_json_obj_get(root, "database");
1146 if (database != NULL) {
1147 value = native_json_obj_get(database, key);
1148 }
1149 }
1150 if (yyjson_is_str(value)) {
1151 copy = dup_range(yyjson_get_str(value), yyjson_get_len(value));
1152 if (copy != NULL) {
1153 fputs(copy, stdout);
1154 }
1155 free(copy);
1156 }
1158 return code;
1159}
1160
1161static int handle_manifest_app_root_command(int argc, char **argv) {
1162 const char *manifest_path = json_option_value_runtime(argc, argv, "--manifest");
1163 yyjson_doc *doc = NULL;
1164 yyjson_val *root = NULL;
1165 char *copy = NULL;
1166 if (manifest_path == NULL) {
1167 return 64;
1168 }
1169 doc = native_json_doc_load_file(manifest_path);
1170 if (doc == NULL) {
1171 return 66;
1172 }
1173 root = yyjson_doc_get_root(doc);
1174 copy = native_json_obj_get_string_dup(root, "app_root");
1175 if (copy != NULL) {
1176 fputs(copy, stdout);
1177 free(copy);
1178 }
1180 return 0;
1181}
1182
1183static yyjson_val *find_form_field_value_runtime(yyjson_val *root, const char *form_id, const char *field_id) {
1184 yyjson_val *forms = native_json_obj_get_array(root, "forms");
1185 yyjson_arr_iter forms_iter;
1186 yyjson_val *form = NULL;
1187 if (forms == NULL || !yyjson_arr_iter_init(forms, &forms_iter)) {
1188 return NULL;
1189 }
1190 while ((form = yyjson_arr_iter_next(&forms_iter)) != NULL) {
1191 yyjson_val *fields = NULL;
1192 yyjson_arr_iter fields_iter;
1193 yyjson_val *field = NULL;
1194 char *candidate_form_id = native_json_obj_get_string_dup(form, "form_id");
1195 if (candidate_form_id == NULL || !streq(candidate_form_id, form_id)) {
1196 free(candidate_form_id);
1197 continue;
1198 }
1199 free(candidate_form_id);
1200 fields = native_json_obj_get_array(form, "fields");
1201 if (fields == NULL || !yyjson_arr_iter_init(fields, &fields_iter)) {
1202 continue;
1203 }
1204 while ((field = yyjson_arr_iter_next(&fields_iter)) != NULL) {
1205 char *candidate_field_id = native_json_obj_get_string_dup(field, "field_id");
1206 int match = candidate_field_id != NULL && streq(candidate_field_id, field_id);
1207 free(candidate_field_id);
1208 if (match) {
1209 return field;
1210 }
1211 }
1212 }
1213 return NULL;
1214}
1215
1216static int handle_form_field_json_command(int argc, char **argv) {
1217 const char *fixture = json_option_value_runtime(argc, argv, "--fixture");
1218 const char *form_id = json_option_value_runtime(argc, argv, "--form-id");
1219 const char *field_id = json_option_value_runtime(argc, argv, "--field-id");
1220 yyjson_doc *doc = NULL;
1221 yyjson_val *root = NULL;
1222 yyjson_val *field = NULL;
1223 int code = 0;
1224 if (fixture == NULL || form_id == NULL || field_id == NULL) {
1225 return 64;
1226 }
1227 doc = native_json_doc_load_file(fixture);
1228 if (doc == NULL) {
1229 return 66;
1230 }
1231 root = yyjson_doc_get_root(doc);
1232 field = find_form_field_value_runtime(root, form_id, field_id);
1233 if (field != NULL) {
1234 code = emit_compact_json_value_runtime(field);
1235 }
1237 return field != NULL ? code : 0;
1238}
1239
1240static int handle_form_field_json_lines_command(int argc, char **argv) {
1241 const char *fixture = json_option_value_runtime(argc, argv, "--fixture");
1242 const char *form_id = json_option_value_runtime(argc, argv, "--form-id");
1243 yyjson_doc *doc = NULL;
1244 yyjson_val *root = NULL;
1245 yyjson_val *forms = NULL;
1246 yyjson_arr_iter forms_iter;
1247 yyjson_val *form = NULL;
1248 int emitted = 0;
1249 if (fixture == NULL || form_id == NULL) {
1250 return 64;
1251 }
1252 doc = native_json_doc_load_file(fixture);
1253 if (doc == NULL) {
1254 return 66;
1255 }
1256 root = yyjson_doc_get_root(doc);
1257 forms = native_json_obj_get_array(root, "forms");
1258 if (forms != NULL && yyjson_arr_iter_init(forms, &forms_iter)) {
1259 while ((form = yyjson_arr_iter_next(&forms_iter)) != NULL) {
1260 yyjson_val *fields = NULL;
1261 yyjson_arr_iter fields_iter;
1262 yyjson_val *field = NULL;
1263 char *candidate_form_id = native_json_obj_get_string_dup(form, "form_id");
1264 if (candidate_form_id == NULL || !streq(candidate_form_id, form_id)) {
1265 free(candidate_form_id);
1266 continue;
1267 }
1268 free(candidate_form_id);
1269 fields = native_json_obj_get_array(form, "fields");
1270 if (fields != NULL && yyjson_arr_iter_init(fields, &fields_iter)) {
1271 while ((field = yyjson_arr_iter_next(&fields_iter)) != NULL) {
1272 char *line = native_json_serialize_compact_dup(field);
1273 if (line != NULL) {
1274 fputs(line, stdout);
1275 fputc('\n', stdout);
1276 free(line);
1277 emitted = 1;
1278 }
1279 }
1280 }
1281 break;
1282 }
1283 }
1285 return emitted ? 0 : 0;
1286}
1287
1288static int handle_form_options_csv_command(int argc, char **argv) {
1289 const char *fixture = json_option_value_runtime(argc, argv, "--fixture");
1290 const char *form_id = json_option_value_runtime(argc, argv, "--form-id");
1291 const char *field_id = json_option_value_runtime(argc, argv, "--field-id");
1292 yyjson_doc *doc = NULL;
1293 yyjson_val *root = NULL;
1294 yyjson_val *field = NULL;
1295 yyjson_val *options = NULL;
1296 yyjson_arr_iter iter;
1297 yyjson_val *option = NULL;
1298 int first = 1;
1299 if (fixture == NULL || form_id == NULL || field_id == NULL) {
1300 return 64;
1301 }
1302 doc = native_json_doc_load_file(fixture);
1303 if (doc == NULL) {
1304 return 66;
1305 }
1306 root = yyjson_doc_get_root(doc);
1307 field = find_form_field_value_runtime(root, form_id, field_id);
1308 options = field != NULL ? native_json_obj_get_array(field, "options") : NULL;
1309 if (options != NULL && yyjson_arr_iter_init(options, &iter)) {
1310 while ((option = yyjson_arr_iter_next(&iter)) != NULL) {
1311 char *value = native_json_obj_get_string_dup(option, "value");
1312 if (value == NULL) {
1313 continue;
1314 }
1315 if (!first) {
1316 fputc(',', stdout);
1317 }
1318 fputs(value, stdout);
1319 first = 0;
1320 free(value);
1321 }
1322 }
1324 return 0;
1325}
1326
1327static int handle_form_options_html_command(int argc, char **argv) {
1328 const char *fixture = json_option_value_runtime(argc, argv, "--fixture");
1329 const char *form_id = json_option_value_runtime(argc, argv, "--form-id");
1330 const char *field_id = json_option_value_runtime(argc, argv, "--field-id");
1331 const char *selected = json_option_value_runtime(argc, argv, "--selected");
1332 yyjson_doc *doc = NULL;
1333 yyjson_val *root = NULL;
1334 yyjson_val *field = NULL;
1335 yyjson_val *options = NULL;
1336 yyjson_arr_iter iter;
1337 yyjson_val *option = NULL;
1338 if (fixture == NULL || form_id == NULL || field_id == NULL) {
1339 return 64;
1340 }
1341 doc = native_json_doc_load_file(fixture);
1342 if (doc == NULL) {
1343 return 66;
1344 }
1345 root = yyjson_doc_get_root(doc);
1346 field = find_form_field_value_runtime(root, form_id, field_id);
1347 options = field != NULL ? native_json_obj_get_array(field, "options") : NULL;
1348 if (options != NULL && yyjson_arr_iter_init(options, &iter)) {
1349 while ((option = yyjson_arr_iter_next(&iter)) != NULL) {
1350 char *value = native_json_obj_get_string_dup(option, "value");
1351 char *label = native_json_obj_get_string_dup(option, "label");
1352 if (value != NULL) {
1353 fprintf(stdout, "<option value=\"%s\"%s>%s</option>", value, (selected != NULL && streq(value, selected)) ? " selected" : "", label != NULL ? label : value);
1354 }
1355 free(value);
1356 free(label);
1357 }
1358 }
1360 return 0;
1361}
1362
1363static int handle_object_field_string_command(int argc, char **argv) {
1364 const char *text = json_option_value_runtime(argc, argv, "--json");
1365 const char *field = json_option_value_runtime(argc, argv, "--field");
1366 yyjson_doc *doc = NULL;
1367 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1368 char *copy = NULL;
1369 if (field == NULL || field[0] == '\0') {
1370 return 64;
1371 }
1372 if (root != NULL && yyjson_is_obj(root)) {
1373 copy = native_json_obj_get_string_dup(root, field);
1374 if (copy != NULL) {
1375 fputs(copy, stdout);
1376 free(copy);
1377 }
1378 }
1380 return 0;
1381}
1382
1383static int handle_object_field_number_command(int argc, char **argv) {
1384 const char *text = json_option_value_runtime(argc, argv, "--json");
1385 const char *field = json_option_value_runtime(argc, argv, "--field");
1386 yyjson_doc *doc = NULL;
1387 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1388 long value = 0;
1389 if (field == NULL || field[0] == '\0') {
1390 return 64;
1391 }
1392 if (root != NULL && yyjson_is_obj(root)) {
1393 value = native_json_obj_get_long_default(root, field, 0, NULL);
1394 }
1395 fprintf(stdout, "%ld", value);
1397 return 0;
1398}
1399
1400static int handle_object_field_text_command(int argc, char **argv) {
1401 const char *text = json_option_value_runtime(argc, argv, "--json");
1402 const char *field = json_option_value_runtime(argc, argv, "--field");
1403 yyjson_doc *doc = NULL;
1404 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1405 yyjson_val *value = NULL;
1406 char *copy = NULL;
1407 if (field == NULL || field[0] == '\0') {
1408 return 64;
1409 }
1410 if (root != NULL && yyjson_is_obj(root)) {
1411 value = native_json_obj_get(root, field);
1412 }
1413 if (value != NULL && !yyjson_is_null(value)) {
1414 copy = json_value_tostring_dup_runtime(value);
1415 if (copy != NULL) {
1416 fputs(copy, stdout);
1417 free(copy);
1418 }
1419 }
1421 return 0;
1422}
1423
1424static int handle_object_array_length_command(int argc, char **argv) {
1425 const char *text = json_option_value_runtime(argc, argv, "--json");
1426 yyjson_doc *doc = NULL;
1427 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1428 size_t count = 0;
1429 if (root != NULL && yyjson_is_arr(root)) {
1430 count = yyjson_arr_size(root);
1431 }
1432 fprintf(stdout, "%lu", (unsigned long)count);
1434 return 0;
1435}
1436
1437static int handle_object_array_all_integers_command(int argc, char **argv) {
1438 const char *text = json_option_value_runtime(argc, argv, "--json");
1439 yyjson_doc *doc = NULL;
1440 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1441 yyjson_arr_iter iter;
1442 yyjson_val *item = NULL;
1443 int ok = 1;
1444 if (root == NULL || !yyjson_is_arr(root)) {
1446 fputs("false", stdout);
1447 return 0;
1448 }
1449 if (yyjson_arr_iter_init(root, &iter)) {
1450 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
1451 if (!(yyjson_is_int(item) || yyjson_is_uint(item))) {
1452 ok = 0;
1453 break;
1454 }
1455 }
1456 }
1457 fputs(ok ? "true" : "false", stdout);
1459 return 0;
1460}
1461
1462static yyjson_val *object_value_at_path_runtime(yyjson_val *root, const char *path) {
1463 char *copy = NULL;
1464 char *segment = NULL;
1465 char *saveptr = NULL;
1466 yyjson_val *current = root;
1467 if (root == NULL || path == NULL || path[0] == '\0') {
1468 return NULL;
1469 }
1470 copy = strdup(path);
1471 if (copy == NULL) {
1472 return NULL;
1473 }
1474 segment = strtok_r(copy, ".", &saveptr);
1475 while (segment != NULL && current != NULL) {
1476 if (!yyjson_is_obj(current)) {
1477 current = NULL;
1478 break;
1479 }
1480 current = native_json_obj_get(current, segment);
1481 segment = strtok_r(NULL, ".", &saveptr);
1482 }
1483 free(copy);
1484 return current;
1485}
1486
1487static int handle_object_path_json_command(int argc, char **argv) {
1488 const char *text = json_option_value_runtime(argc, argv, "--json");
1489 const char *path = json_option_value_runtime(argc, argv, "--path");
1490 yyjson_doc *doc = NULL;
1491 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1492 yyjson_val *value = NULL;
1493 int code = 0;
1494 if (path == NULL || path[0] == '\0') {
1495 return 64;
1496 }
1497 value = object_value_at_path_runtime(root, path);
1498 if (value == NULL) {
1499 fputs("null", stdout);
1501 return 0;
1502 }
1503 code = emit_compact_json_value_runtime(value);
1505 return code;
1506}
1507
1508static int handle_object_path_string_command(int argc, char **argv) {
1509 const char *text = json_option_value_runtime(argc, argv, "--json");
1510 const char *path = json_option_value_runtime(argc, argv, "--path");
1511 yyjson_doc *doc = NULL;
1512 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1513 yyjson_val *value = NULL;
1514 char *copy = NULL;
1515 if (path == NULL || path[0] == '\0') {
1516 return 64;
1517 }
1518 value = object_value_at_path_runtime(root, path);
1519 if (yyjson_is_str(value)) {
1520 copy = dup_range(yyjson_get_str(value), yyjson_get_len(value));
1521 if (copy != NULL) {
1522 fputs(copy, stdout);
1523 free(copy);
1524 }
1525 }
1527 return 0;
1528}
1529
1530static int handle_object_path_number_command(int argc, char **argv) {
1531 const char *text = json_option_value_runtime(argc, argv, "--json");
1532 const char *path = json_option_value_runtime(argc, argv, "--path");
1533 yyjson_doc *doc = NULL;
1534 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1535 yyjson_val *value = NULL;
1536 long number = 0;
1537 if (path == NULL || path[0] == '\0') {
1538 return 64;
1539 }
1540 value = object_value_at_path_runtime(root, path);
1541 if (value != NULL) {
1542 if (yyjson_is_int(value) || yyjson_is_uint(value) || yyjson_is_real(value)) {
1543 number = yyjson_get_sint(value);
1544 }
1545 }
1546 fprintf(stdout, "%ld", number);
1548 return 0;
1549}
1550
1551static int handle_object_path_bool_command(int argc, char **argv) {
1552 const char *text = json_option_value_runtime(argc, argv, "--json");
1553 const char *path = json_option_value_runtime(argc, argv, "--path");
1554 yyjson_doc *doc = NULL;
1555 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1556 yyjson_val *value = NULL;
1557 int bool_value = 0;
1558 if (path == NULL || path[0] == '\0') {
1559 return 64;
1560 }
1561 value = object_value_at_path_runtime(root, path);
1562 if (yyjson_is_bool(value)) {
1563 bool_value = yyjson_get_bool(value) ? 1 : 0;
1564 }
1565 fputs(bool_value ? "true" : "false", stdout);
1567 return 0;
1568}
1569
1570static int handle_object_path_array_length_command(int argc, char **argv) {
1571 const char *text = json_option_value_runtime(argc, argv, "--json");
1572 const char *path = json_option_value_runtime(argc, argv, "--path");
1573 yyjson_doc *doc = NULL;
1574 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1575 yyjson_val *value = NULL;
1576 size_t count = 0;
1577 if (path == NULL || path[0] == '\0') {
1578 return 64;
1579 }
1580 value = object_value_at_path_runtime(root, path);
1581 if (value != NULL && yyjson_is_arr(value)) {
1582 count = yyjson_arr_size(value);
1583 }
1584 fprintf(stdout, "%lu", (unsigned long)count);
1586 return 0;
1587}
1588
1589static int handle_object_array_item_json_lines_command(int argc, char **argv) {
1590 const char *text = json_option_value_runtime(argc, argv, "--json");
1591 yyjson_doc *doc = NULL;
1592 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1593 yyjson_arr_iter iter;
1594 yyjson_val *item = NULL;
1595 if (root != NULL && yyjson_is_arr(root) && yyjson_arr_iter_init(root, &iter)) {
1596 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
1597 char *line = native_json_serialize_compact_dup(item);
1598 if (line != NULL) {
1599 fputs(line, stdout);
1600 fputc('\n', stdout);
1601 free(line);
1602 }
1603 }
1604 }
1606 return 0;
1607}
1608
1609static int handle_object_array_lines_command(int argc, char **argv) {
1610 const char *text = json_option_value_runtime(argc, argv, "--json");
1611 yyjson_doc *doc = NULL;
1612 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1613 yyjson_arr_iter iter;
1614 yyjson_val *item = NULL;
1615 if (root != NULL && yyjson_is_arr(root) && yyjson_arr_iter_init(root, &iter)) {
1616 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
1617 if (yyjson_is_str(item)) {
1618 fwrite(yyjson_get_str(item), 1, yyjson_get_len(item), stdout);
1619 } else {
1620 char *line = native_json_serialize_compact_dup(item);
1621 if (line != NULL) {
1622 fputs(line, stdout);
1623 free(line);
1624 }
1625 }
1626 fputc('\n', stdout);
1627 }
1628 }
1630 return 0;
1631}
1632
1634 const char *text = json_option_value_runtime(argc, argv, "--json");
1635 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
1636 yyjson_doc *doc = NULL;
1637 yyjson_val *root = load_root_from_text_runtime(text, &doc);
1638 yyjson_mut_doc *mut_doc = NULL;
1639 yyjson_mut_val *array = NULL;
1640 yyjson_arr_iter iter;
1641 yyjson_val *item = NULL;
1642 int code = 69;
1643 if (app_root == NULL) {
1644 return 64;
1645 }
1646 if (root == NULL || !yyjson_is_arr(root)) {
1647 fputs("[]", stdout);
1649 return 0;
1650 }
1651 mut_doc = yyjson_mut_doc_new(NULL);
1652 array = yyjson_mut_arr(mut_doc);
1653 if (mut_doc == NULL || array == NULL) {
1655 yyjson_mut_doc_free(mut_doc);
1656 return 69;
1657 }
1658 yyjson_mut_doc_set_root(mut_doc, array);
1659 if (yyjson_arr_iter_init(root, &iter)) {
1660 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
1661 if (yyjson_is_str(item)) {
1662 const char *value = yyjson_get_str(item);
1663 size_t len = yyjson_get_len(item);
1664 if (value != NULL && len > 0 && value[0] == '/') {
1665 yyjson_mut_arr_append(array, yyjson_mut_strncpy(mut_doc, value, len));
1666 } else if (value != NULL) {
1667 size_t needed = strlen(app_root) + 1 + len + 1;
1668 char *joined = (char *)malloc(needed);
1669 if (joined == NULL) {
1671 yyjson_mut_doc_free(mut_doc);
1672 return 69;
1673 }
1674 snprintf(joined, needed, "%s/%s", app_root, value);
1675 yyjson_mut_arr_append(array, yyjson_mut_strcpy(mut_doc, joined));
1676 free(joined);
1677 }
1678 }
1679 }
1680 }
1683 yyjson_mut_doc_free(mut_doc);
1684 return code;
1685}
1686
1687static int handle_file_compact_object_command(int argc, char **argv) {
1688 const char *file_path = json_option_value_runtime(argc, argv, "--file");
1689 yyjson_doc *doc = NULL;
1690 yyjson_val *root = NULL;
1691 int code = 0;
1692 if (file_path == NULL || file_path[0] == '\0') {
1693 return 64;
1694 }
1695 doc = native_json_doc_load_file(file_path);
1696 if (doc == NULL) {
1697 return 66;
1698 }
1699 root = yyjson_doc_get_root(doc);
1700 if (root == NULL || !yyjson_is_obj(root)) {
1701 fputs("{}", stdout);
1703 return 0;
1704 }
1707 return code;
1708}
1709
1710static int handle_file_field_string_command(int argc, char **argv) {
1711 const char *file_path = json_option_value_runtime(argc, argv, "--file");
1712 const char *field = json_option_value_runtime(argc, argv, "--field");
1713 yyjson_doc *doc = NULL;
1714 yyjson_val *root = NULL;
1715 char *copy = NULL;
1716 if (file_path == NULL || file_path[0] == '\0' || field == NULL || field[0] == '\0') {
1717 return 64;
1718 }
1719 doc = native_json_doc_load_file(file_path);
1720 if (doc == NULL) {
1721 return 66;
1722 }
1723 root = yyjson_doc_get_root(doc);
1724 if (root != NULL && yyjson_is_obj(root)) {
1725 copy = native_json_obj_get_string_dup(root, field);
1726 if (copy != NULL) {
1727 fputs(copy, stdout);
1728 free(copy);
1729 }
1730 }
1732 return 0;
1733}
1734
1735static int handle_file_field_number_command(int argc, char **argv) {
1736 const char *file_path = json_option_value_runtime(argc, argv, "--file");
1737 const char *field = json_option_value_runtime(argc, argv, "--field");
1738 yyjson_doc *doc = NULL;
1739 yyjson_val *root = NULL;
1740 long value = 0;
1741 if (file_path == NULL || file_path[0] == '\0' || field == NULL || field[0] == '\0') {
1742 return 64;
1743 }
1744 doc = native_json_doc_load_file(file_path);
1745 if (doc == NULL) {
1746 return 66;
1747 }
1748 root = yyjson_doc_get_root(doc);
1749 if (root != NULL && yyjson_is_obj(root)) {
1750 value = native_json_obj_get_long_default(root, field, 0, NULL);
1751 }
1752 fprintf(stdout, "%ld", value);
1754 return 0;
1755}
1756
1757static int handle_file_section_length_command(int argc, char **argv) {
1758 const char *file_path = json_option_value_runtime(argc, argv, "--file");
1759 const char *section = json_option_value_runtime(argc, argv, "--section");
1760 yyjson_doc *doc = NULL;
1761 yyjson_val *root = NULL;
1762 yyjson_val *array_value = NULL;
1763 size_t count = 0;
1764 if (file_path == NULL || file_path[0] == '\0' || section == NULL || section[0] == '\0') {
1765 return 64;
1766 }
1767 doc = native_json_doc_load_file(file_path);
1768 if (doc == NULL) {
1769 return 66;
1770 }
1771 root = yyjson_doc_get_root(doc);
1772 array_value = native_json_obj_get_array(root, section);
1773 if (array_value != NULL) {
1774 count = yyjson_arr_size(array_value);
1775 }
1776 fprintf(stdout, "%lu", (unsigned long)count);
1778 return 0;
1779}
1780
1781/**
1782 * @brief Returns non-zero when a dynamic config key should resolve relative to its owning config file.
1783 * @param key Config key name.
1784 * @return Non-zero when the key is path-bearing, otherwise 0.
1785 */
1786static int dynamic_config_key_is_path_runtime(const char *key) {
1787 return key != NULL && (
1788 streq(key, "db_path") ||
1789 streq(key, "db_tls_root_cert") ||
1790 streq(key, "db_tls_cert") ||
1791 streq(key, "db_tls_key")
1792 );
1793}
1794
1795/**
1796 * @brief Resolves one raw dynamic config value against its owning config file when needed.
1797 * @param conf_path Path to the config file that supplied the value.
1798 * @param key Config key name.
1799 * @param raw_value Raw config value.
1800 * @return Newly allocated resolved value, or NULL when no value is available.
1801 */
1802static char *resolve_dynamic_config_value_runtime(const char *conf_path, const char *key, const char *raw_value) {
1803 if (raw_value == NULL) {
1804 return NULL;
1805 }
1807 return resolve_conf_relative_path_native(conf_path, raw_value);
1808 }
1809 return strdup(raw_value);
1810}
1811
1812/**
1813 * @brief Loads one raw config value from a path when the file exists.
1814 * @param conf_path Config file path.
1815 * @param key Config key name.
1816 * @return Newly allocated raw value, or NULL when the key or file is missing.
1817 */
1818static char *dynamic_conf_lookup_runtime(const char *conf_path, const char *key) {
1819 if (conf_path == NULL || conf_path[0] == '\0' || key == NULL || key[0] == '\0' || !path_exists(conf_path)) {
1820 return NULL;
1821 }
1822 return conf_lookup_value(conf_path, key);
1823}
1824
1825/**
1826 * @brief Loads one merged dynamic config value with runtime-conf precedence over app-local config.
1827 * @param app_root App root directory.
1828 * @param runtime_conf_path Runtime config path from the environment.
1829 * @param key Config key name.
1830 * @return Newly allocated resolved value, or NULL when the key is absent.
1831 */
1832static char *dynamic_merged_config_value_runtime(const char *app_root, const char *runtime_conf_path, const char *key) {
1833 char *raw_value = NULL;
1834 char *resolved_value = NULL;
1835 char *app_conf_path = NULL;
1836 if (key == NULL || key[0] == '\0') {
1837 return NULL;
1838 }
1839 raw_value = dynamic_conf_lookup_runtime(runtime_conf_path, key);
1840 if (raw_value != NULL) {
1841 resolved_value = resolve_dynamic_config_value_runtime(runtime_conf_path, key, raw_value);
1842 free(raw_value);
1843 return resolved_value;
1844 }
1845 app_conf_path = app_local_conf_path_native(app_root);
1846 raw_value = dynamic_conf_lookup_runtime(app_conf_path, key);
1847 if (raw_value != NULL) {
1848 resolved_value = resolve_dynamic_config_value_runtime(app_conf_path, key, raw_value);
1849 free(raw_value);
1850 }
1851 free(app_conf_path);
1852 return resolved_value;
1853}
1854
1855/**
1856 * @brief Emits one merged dynamic config value.
1857 * @param argc Command argument count.
1858 * @param argv Command argument vector.
1859 * @return Native status code.
1860 */
1861static int handle_dynamic_config_value_command(int argc, char **argv) {
1862 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
1863 const char *runtime_conf_path = json_option_value_runtime(argc, argv, "--runtime-conf");
1864 const char *key = json_option_value_runtime(argc, argv, "--key");
1865 char *value = NULL;
1866 if (app_root == NULL || app_root[0] == '\0' || key == NULL || key[0] == '\0') {
1867 return 64;
1868 }
1869 value = native_dynamic_config_merged_value_dup(app_root, runtime_conf_path, key);
1870 if (value != NULL) {
1871 fputs(value, stdout);
1872 }
1873 free(value);
1874 return 0;
1875}
1876
1877/**
1878 * @brief Emits the configured dynamic relational backend when it is recognized.
1879 * @param argc Command argument count.
1880 * @param argv Command argument vector.
1881 * @return Native status code.
1882 */
1883static int handle_dynamic_relational_backend_command(int argc, char **argv) {
1884 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
1885 const char *runtime_conf_path = json_option_value_runtime(argc, argv, "--runtime-conf");
1886 char *backend = NULL;
1887 if (app_root == NULL || app_root[0] == '\0') {
1888 return 64;
1889 }
1890 backend = native_dynamic_relational_backend_dup(app_root, runtime_conf_path);
1891 if (backend != NULL) {
1892 fputs(backend, stdout);
1893 }
1894 free(backend);
1895 return 0;
1896}
1897
1898/**
1899 * @brief Emits the effective sqlite path for a dynamic app.
1900 * @param argc Command argument count.
1901 * @param argv Command argument vector.
1902 * @return Native status code.
1903 */
1904static int handle_dynamic_relational_sqlite_path_command(int argc, char **argv) {
1905 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
1906 const char *runtime_conf_path = json_option_value_runtime(argc, argv, "--runtime-conf");
1907 const char *app_id = json_option_value_runtime(argc, argv, "--app-id");
1908 char *sqlite_path = NULL;
1909 if (app_root == NULL || app_root[0] == '\0') {
1910 return 64;
1911 }
1912 sqlite_path = native_dynamic_relational_sqlite_path_dup(app_root, runtime_conf_path, app_id);
1913 if (sqlite_path != NULL) {
1914 fputs(sqlite_path, stdout);
1915 }
1916 free(sqlite_path);
1917 return 0;
1918}
1919
1920/**
1921 * @brief Emits the safe Pharos filename for an uploaded media candidate.
1922 * @param argc Command argument count.
1923 * @param argv Command argument vector.
1924 * @return Native status code.
1925 */
1926static int handle_dynamic_safe_upload_filename_command(int argc, char **argv) {
1927 const char *raw_name = json_option_value_runtime(argc, argv, "--name");
1928 char *safe_name = safe_upload_filename_dup(raw_name);
1929 if (safe_name == NULL) {
1930 return 69;
1931 }
1932 fputs(safe_name, stdout);
1933 free(safe_name);
1934 return 0;
1935}
1936
1937/**
1938 * @brief Emits the on-disk artifact path for a managed media URL.
1939 * @param argc Command argument count.
1940 * @param argv Command argument vector.
1941 * @return Native status code.
1942 */
1943static int handle_dynamic_media_disk_path_command(int argc, char **argv) {
1944 const char *artifact_dir = json_option_value_runtime(argc, argv, "--artifact-dir");
1945 const char *media_url = json_option_value_runtime(argc, argv, "--media-url");
1946 char *disk_path = media_disk_path_dup(artifact_dir, media_url);
1947 if (disk_path != NULL) {
1948 fputs(disk_path, stdout);
1949 }
1950 free(disk_path);
1951 return 0;
1952}
1953
1954/**
1955 * @brief Ensures the managed business media subtree exists under an app artifact.
1956 * @param argc Command argument count.
1957 * @param argv Command argument vector.
1958 * @return Native status code.
1959 */
1960static int handle_dynamic_ensure_business_media_tree_command(int argc, char **argv) {
1961 const char *artifact_dir = json_option_value_runtime(argc, argv, "--artifact-dir");
1962 const char *business_slug = json_option_value_runtime(argc, argv, "--business-slug");
1963 return ensure_business_media_tree(artifact_dir, business_slug);
1964}
1965
1966/**
1967 * @brief Removes one managed business media file from an app artifact when it exists.
1968 * @param argc Command argument count.
1969 * @param argv Command argument vector.
1970 * @return Native status code.
1971 */
1972static int handle_dynamic_remove_media_url_file_command(int argc, char **argv) {
1973 const char *artifact_dir = json_option_value_runtime(argc, argv, "--artifact-dir");
1974 const char *media_url = json_option_value_runtime(argc, argv, "--media-url");
1975 return remove_media_url_file(artifact_dir, media_url);
1976}
1977
1978/**
1979 * @brief Extracts one multipart upload into the app artifact media tree and returns the resulting media URL.
1980 * @param argc Command argument count.
1981 * @param argv Command argument vector.
1982 * @return Native status code.
1983 */
1984static int handle_dynamic_store_uploaded_media_command(int argc, char **argv) {
1985 const char *artifact_dir = json_option_value_runtime(argc, argv, "--artifact-dir");
1986 const char *content_type = json_option_value_runtime(argc, argv, "--content-type");
1987 const char *body_path = json_option_value_runtime(argc, argv, "--request-body-path");
1988 const char *field_name = json_option_value_runtime(argc, argv, "--field");
1989 const char *target_url_dir = json_option_value_runtime(argc, argv, "--target-url-dir");
1990 const char *fallback_filename = json_option_value_runtime(argc, argv, "--fallback-filename");
1991 char *result = store_uploaded_media(artifact_dir, content_type, body_path, field_name, target_url_dir, fallback_filename);
1992 if (result != NULL) {
1993 fputs(result, stdout);
1994 free(result);
1995 return 0;
1996 }
1997 return 64;
1998}
1999
2000/**
2001 * @brief Tests whether an executable command name resolves through PATH.
2002 * @param command Command name to resolve.
2003 * @return Non-zero when the command is available, otherwise 0.
2004 */
2005static char *resolve_command_path_runtime(const char *command) {
2006 char *path_copy = NULL;
2007 char *segment = NULL;
2008 char *state = NULL;
2009 const char *path_env = NULL;
2010 if (command == NULL || command[0] == '\0') {
2011 return NULL;
2012 }
2013 if (strchr(command, '/') != NULL) {
2014 return access(command, X_OK) == 0 ? strdup(command) : NULL;
2015 }
2016 path_env = getenv("PATH");
2017 if (path_env == NULL || path_env[0] == '\0') {
2018 return NULL;
2019 }
2020 path_copy = strdup(path_env);
2021 if (path_copy == NULL) {
2022 return NULL;
2023 }
2024 for (segment = strtok_r(path_copy, ":", &state); segment != NULL; segment = strtok_r(NULL, ":", &state)) {
2025 char *candidate = path_join(segment, command);
2026 if (candidate != NULL && access(candidate, X_OK) == 0) {
2027 free(path_copy);
2028 return candidate;
2029 }
2030 free(candidate);
2031 }
2032 free(path_copy);
2033 return NULL;
2034}
2035
2036static int command_exists_in_path_runtime(const char *command) {
2037 char *resolved = resolve_command_path_runtime(command);
2038 int present = resolved != NULL && resolved[0] != '\0';
2039 free(resolved);
2040 return present;
2041}
2042
2047
2048/**
2049 * @brief Captures sqlite result rows into the shared text buffer using CLI-compatible separators.
2050 * @param context_void Capture buffer context.
2051 * @param column_count Result column count for the current row.
2052 * @param column_values Result column text values.
2053 * @param column_names Unused sqlite column names.
2054 * @return 0 to continue sqlite execution.
2055 */
2056static int sqlite_capture_callback_runtime(void *context_void, int column_count, char **column_values, char **column_names) {
2057 SqliteCaptureRuntime *context = (SqliteCaptureRuntime *)context_void;
2058 int index = 0;
2059 (void)column_names;
2060 if (context == NULL || context->buffer == NULL) {
2061 return 1;
2062 }
2063 if (context->row_count > 0 && !text_buffer_append_text_runtime(context->buffer, "\n")) {
2064 return 1;
2065 }
2066 for (index = 0; index < column_count; index++) {
2067 const char *value = (column_values != NULL && column_values[index] != NULL) ? column_values[index] : "";
2068 if (index > 0 && !text_buffer_append_text_runtime(context->buffer, "|")) {
2069 return 1;
2070 }
2071 if (!text_buffer_append_text_runtime(context->buffer, value)) {
2072 return 1;
2073 }
2074 }
2075 context->row_count++;
2076 return 0;
2077}
2078
2079/**
2080 * @brief Executes one sqlite SQL batch through the native sqlite3 C API.
2081 * @param sqlite_path Database path.
2082 * @param sql_text SQL batch text.
2083 * @param output_out Optional captured stdout-style row text.
2084 * @return Native status code.
2085 */
2086static int run_sqlite_command_runtime(const char *sqlite_path, const char *sql_text, char **output_out) {
2087 sqlite3 *db = NULL;
2088 char *error_message = NULL;
2089 TextBufferRuntime buffer = {0};
2090 SqliteCaptureRuntime capture = {0};
2091 int sqlite_code = SQLITE_ERROR;
2092 int code = 69;
2093 if (output_out != NULL) {
2094 *output_out = NULL;
2095 }
2096 if (sqlite_path == NULL || sqlite_path[0] == '\0' || sql_text == NULL) {
2097 return 64;
2098 }
2099 if (sqlite3_open(sqlite_path, &db) != SQLITE_OK || db == NULL) {
2100 if (db != NULL) {
2101 sqlite3_close(db);
2102 }
2103 return 69;
2104 }
2105 capture.buffer = &buffer;
2106 sqlite_code = sqlite3_exec(
2107 db,
2108 sql_text,
2109 output_out != NULL ? sqlite_capture_callback_runtime : NULL,
2110 output_out != NULL ? &capture : NULL,
2111 &error_message
2112 );
2113 if (sqlite_code == SQLITE_OK) {
2114 code = 0;
2115 if (output_out != NULL) {
2116 *output_out = text_buffer_take_runtime(&buffer);
2117 if (*output_out == NULL) {
2118 *output_out = strdup("");
2119 }
2120 if (*output_out != NULL) {
2121 trim_in_place(*output_out);
2122 } else {
2123 code = 69;
2124 }
2125 }
2126 }
2127 sqlite3_free(error_message);
2128 text_buffer_free_runtime(&buffer);
2129 sqlite3_close(db);
2130 return code;
2131}
2132
2133static char *sqlite_query_value_runtime(const char *sqlite_path, const char *sql_text) {
2134 char *output = NULL;
2135 if (run_sqlite_command_runtime(sqlite_path, sql_text, &output) != 0) {
2136 free(output);
2137 return NULL;
2138 }
2139 return output;
2140}
2141
2143 if (buffer == NULL) {
2144 return 0;
2145 }
2146 switch (value) {
2147 case '\'':
2148 return text_buffer_append_text_runtime(buffer, "''");
2149 default:
2150 return text_buffer_reserve_runtime(buffer, buffer->length + 1) ? (buffer->text[buffer->length++] = value, buffer->text[buffer->length] = '\0', 1) : 0;
2151 }
2152}
2153
2154static char *sql_literal_escape_dup_runtime(const char *value) {
2155 TextBufferRuntime buffer = {0};
2156 const char *cursor = value != NULL ? value : "";
2157 while (*cursor != '\0') {
2158 if (!append_escaped_sql_char_runtime(&buffer, *cursor)) {
2159 text_buffer_free_runtime(&buffer);
2160 return NULL;
2161 }
2162 cursor++;
2163 }
2164 return text_buffer_take_runtime(&buffer);
2165}
2166
2167static int write_temp_text_file_runtime(const char *text, char **path_out) {
2168 char path_template[] = "/tmp/pharos-runtime-XXXXXX";
2169 FILE *file = NULL;
2170 int fd;
2171 size_t text_len = text != NULL ? strlen(text) : 0;
2172 if (path_out == NULL) {
2173 return 69;
2174 }
2175 *path_out = NULL;
2176 fd = mkstemp(path_template);
2177 if (fd < 0) {
2178 return 69;
2179 }
2180 file = fdopen(fd, "wb");
2181 if (file == NULL) {
2182 close(fd);
2183 remove(path_template);
2184 return 69;
2185 }
2186 if (text_len > 0 && fwrite(text, 1, text_len, file) != text_len) {
2187 fclose(file);
2188 remove(path_template);
2189 return 69;
2190 }
2191 if (fclose(file) != 0) {
2192 remove(path_template);
2193 return 69;
2194 }
2195 *path_out = strdup(path_template);
2196 if (*path_out == NULL) {
2197 remove(path_template);
2198 return 69;
2199 }
2200 return 0;
2201}
2202
2203static char *capture_checksum_output_runtime(const char *command, const char *arg1, const char *arg2, const char *file_path, int *code_out) {
2204 char *resolved_path = NULL;
2205 char *argv_list[5];
2206 int argc = 0;
2207 char *output = NULL;
2208 if (code_out != NULL) {
2209 *code_out = 69;
2210 }
2211 if (command == NULL || file_path == NULL) {
2212 return NULL;
2213 }
2214 resolved_path = resolve_command_path_runtime(command);
2215 if (resolved_path == NULL) {
2216 return NULL;
2217 }
2218 argv_list[argc++] = resolved_path;
2219 if (arg1 != NULL) {
2220 argv_list[argc++] = (char *)arg1;
2221 }
2222 if (arg2 != NULL) {
2223 argv_list[argc++] = (char *)arg2;
2224 }
2225 argv_list[argc++] = (char *)file_path;
2226 argv_list[argc] = NULL;
2227 output = NULL;
2228 if (capture_execv_output_native(argv_list, &output, NULL) == 0 && output != NULL) {
2229 trim_in_place(output);
2230 if (code_out != NULL) {
2231 *code_out = 0;
2232 }
2233 }
2234 free(resolved_path);
2235 return output;
2236}
2237
2238static char *checksum_string_runtime(const char *value) {
2239 static const struct {
2240 const char *command;
2241 const char *arg1;
2242 const char *arg2;
2243 } checksum_tools[] = {{"sha256sum", NULL, NULL}, {"shasum", "-a", "256"}};
2244 size_t index = 0;
2245 char *tmp_path = NULL;
2246 char *output = NULL;
2247 int code = 69;
2248 if (write_temp_text_file_runtime(value != NULL ? value : "", &tmp_path) != 0) {
2249 return NULL;
2250 }
2251 for (index = 0; index < sizeof(checksum_tools) / sizeof(checksum_tools[0]); index++) {
2252 if (!command_exists_in_path_runtime(checksum_tools[index].command)) {
2253 continue;
2254 }
2255 output = capture_checksum_output_runtime(checksum_tools[index].command, checksum_tools[index].arg1, checksum_tools[index].arg2, tmp_path, &code);
2256 break;
2257 }
2258 remove(tmp_path);
2259 free(tmp_path);
2260 if (code != 0 || output == NULL) {
2261 free(output);
2262 return NULL;
2263 }
2264 {
2265 char *space = strchr(output, ' ');
2266 if (space != NULL) {
2267 *space = '\0';
2268 }
2269 }
2270 return output;
2271}
2272
2273static char *cksum_signature_runtime(const char *value) {
2274 char *tmp_path = NULL;
2275 char *output = NULL;
2276 char *signature = NULL;
2277 int code = 69;
2278 if (write_temp_text_file_runtime(value != NULL ? value : "", &tmp_path) != 0) {
2279 return NULL;
2280 }
2281 output = capture_checksum_output_runtime("cksum", NULL, NULL, tmp_path, &code);
2282 remove(tmp_path);
2283 free(tmp_path);
2284 if (code != 0 || output == NULL) {
2285 free(output);
2286 return NULL;
2287 }
2288 {
2289 char *first_space = strchr(output, ' ');
2290 char *length_text = NULL;
2291 if (first_space != NULL) {
2292 *first_space = '\0';
2293 length_text = first_space + 1;
2294 while (*length_text == ' ') {
2295 length_text++;
2296 }
2297 first_space = strchr(length_text, ' ');
2298 if (first_space != NULL) {
2299 *first_space = '\0';
2300 }
2301 signature = (char *)malloc(strlen(output) + strlen(length_text) + 2);
2302 if (signature != NULL) {
2303 snprintf(signature, strlen(output) + strlen(length_text) + 2, "%s:%s", output, length_text);
2304 }
2305 }
2306 }
2307 free(output);
2308 return signature;
2309}
2310
2311static char *dynamic_sqlite_path_dup_runtime(const char *app_root, const char *runtime_conf_path, const char *app_id) {
2312 char *sqlite_path = NULL;
2313 if (app_root == NULL || app_root[0] == '\0') {
2314 return NULL;
2315 }
2316 sqlite_path = dynamic_merged_config_value_runtime(app_root, runtime_conf_path, "db_path");
2317 if (sqlite_path != NULL && sqlite_path[0] != '\0') {
2318 return sqlite_path;
2319 }
2320 free(sqlite_path);
2321 {
2322 char buffer[4096];
2323 snprintf(buffer, sizeof(buffer), "/var/lib/pharos/%s.sqlite3", app_id != NULL && app_id[0] != '\0' ? app_id : "app");
2324 return strdup(buffer);
2325 }
2326}
2327
2328static int sqlite_apply_file_list_runtime(const char *sqlite_path, const char *app_root, yyjson_val *list) {
2329 yyjson_arr_iter iter;
2330 yyjson_val *item = NULL;
2331 if (sqlite_path == NULL || app_root == NULL || list == NULL) {
2332 return 0;
2333 }
2334 if (!yyjson_arr_iter_init(list, &iter)) {
2335 return 69;
2336 }
2337 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
2338 char *resolved_path = NULL;
2339 char *sql_text = NULL;
2340 if (!yyjson_is_str(item)) {
2341 return 69;
2342 }
2343 resolved_path = resolve_relative_to(app_root, yyjson_get_str(item));
2344 sql_text = resolved_path != NULL ? read_file_text(resolved_path) : NULL;
2345 free(resolved_path);
2346 if (sql_text == NULL) {
2347 free(sql_text);
2348 return 66;
2349 }
2350 if (run_sqlite_command_runtime(sqlite_path, sql_text, NULL) != 0) {
2351 free(sql_text);
2352 return 69;
2353 }
2354 free(sql_text);
2355 }
2356 return 0;
2357}
2358
2359/**
2360 * @brief Applies one declarative SQL file list through the native PostgreSQL driver.
2361 * @param config Resolved PostgreSQL runtime connection settings.
2362 * @param app_root App root directory used to resolve relative SQL file paths.
2363 * @param list Declarative SQL file list array.
2364 * @return Native status code.
2365 */
2366static int postgresql_apply_file_list_runtime(const DynamicPostgresqlRuntimeConfig *config, const char *app_root, yyjson_val *list) {
2367 yyjson_arr_iter iter;
2368 yyjson_val *item = NULL;
2369 if (config == NULL || app_root == NULL || list == NULL) {
2370 return 0;
2371 }
2372 if (!yyjson_arr_iter_init(list, &iter)) {
2373 return 69;
2374 }
2375 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
2376 char *resolved_path = NULL;
2377 char *sql_text = NULL;
2378 if (!yyjson_is_str(item)) {
2379 return 69;
2380 }
2381 resolved_path = resolve_relative_to(app_root, yyjson_get_str(item));
2382 sql_text = resolved_path != NULL ? read_file_text(resolved_path) : NULL;
2383 free(resolved_path);
2384 if (sql_text == NULL) {
2385 free(sql_text);
2386 return 66;
2387 }
2388 if (run_postgresql_sql_capture_runtime(config, sql_text, 0, NULL) != 0) {
2389 free(sql_text);
2390 return 69;
2391 }
2392 free(sql_text);
2393 }
2394 return 0;
2395}
2396
2397static char *migration_expected_checksum_dup_runtime(const char *app_root, yyjson_val *migration_record, const char *backend) {
2398 yyjson_val *backend_files = NULL;
2399 yyjson_val *backend_record = NULL;
2400 yyjson_val *forward_files = NULL;
2401 yyjson_arr_iter iter;
2402 yyjson_val *item = NULL;
2403 TextBufferRuntime combined = {0};
2404 char *forward_checksum = NULL;
2405 char *payload = NULL;
2406 char *migration_id = NULL;
2407 char *result = NULL;
2408 if (app_root == NULL || migration_record == NULL || backend == NULL) {
2409 return NULL;
2410 }
2411 backend_files = native_json_obj_get(migration_record, "backend_files");
2412 backend_record = backend_files != NULL && yyjson_is_obj(backend_files) ? native_json_obj_get(backend_files, backend) : NULL;
2413 forward_files = backend_record != NULL ? native_json_obj_get_array(backend_record, "forward_sql_files") : NULL;
2414 if (forward_files != NULL && yyjson_arr_iter_init(forward_files, &iter)) {
2415 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
2416 char *resolved_path = NULL;
2417 char *file_text = NULL;
2418 if (!yyjson_is_str(item)) {
2419 text_buffer_free_runtime(&combined);
2420 return NULL;
2421 }
2422 resolved_path = resolve_relative_to(app_root, yyjson_get_str(item));
2423 if (resolved_path != NULL && path_exists(resolved_path)) {
2424 file_text = read_file_text(resolved_path);
2425 if (file_text != NULL && !text_buffer_append_text_runtime(&combined, file_text)) {
2426 free(file_text);
2427 free(resolved_path);
2428 text_buffer_free_runtime(&combined);
2429 return NULL;
2430 }
2431 free(file_text);
2432 }
2433 if (resolved_path != NULL && !text_buffer_append_format_runtime(&combined, "\n-- pharos-path: %s\n", resolved_path)) {
2434 free(resolved_path);
2435 text_buffer_free_runtime(&combined);
2436 return NULL;
2437 }
2438 free(resolved_path);
2439 }
2440 }
2441 forward_checksum = cksum_signature_runtime(combined.text != NULL ? combined.text : "");
2442 text_buffer_free_runtime(&combined);
2443 migration_id = native_json_obj_get_string_dup(migration_record, "migration_id");
2444 if (migration_id == NULL || forward_checksum == NULL) {
2445 free(migration_id);
2446 free(forward_checksum);
2447 return NULL;
2448 }
2449 payload = (char *)malloc(strlen(migration_id) + strlen(backend) + strlen(forward_checksum) + 64);
2450 if (payload != NULL) {
2451 snprintf(payload, strlen(migration_id) + strlen(backend) + strlen(forward_checksum) + 64, "migration_id=%s\nbackend=%s\nforward_checksum=%s", migration_id, backend, forward_checksum);
2452 result = checksum_string_runtime(payload);
2453 }
2454 free(payload);
2455 free(migration_id);
2456 free(forward_checksum);
2457 return result;
2458}
2459
2460/**
2461 * @brief Resolves the declarative sqlite export query path for UCAL runtime-state bridge export.
2462 * @param app_root App root directory.
2463 * @return Newly allocated absolute path, or NULL when it cannot be formed.
2464 */
2465static char *ucal_runtime_state_export_sqlite_path_dup_runtime(const char *app_root) {
2466 return app_root != NULL ? path_join(app_root, "data/framework/runtime-state-export-sqlite.sql") : NULL;
2467}
2468
2469/**
2470 * @brief Resolves the declarative sqlite import query path for UCAL runtime-state bridge import.
2471 * @param app_root App root directory.
2472 * @return Newly allocated absolute path, or NULL when it cannot be formed.
2473 */
2474static char *ucal_runtime_state_import_sqlite_path_dup_runtime(const char *app_root) {
2475 return app_root != NULL ? path_join(app_root, "data/framework/runtime-state-import-sqlite.sql") : NULL;
2476}
2477
2478/**
2479 * @brief Resolves the declarative postgresql export query path for UCAL runtime-state bridge export.
2480 * @param app_root App root directory.
2481 * @return Newly allocated absolute path, or NULL when it cannot be formed.
2482 */
2484 return app_root != NULL ? path_join(app_root, "data/framework/runtime-state-export-postgresql.sql") : NULL;
2485}
2486
2487/**
2488 * @brief Resolves the declarative postgresql import query path for UCAL runtime-state bridge import.
2489 * @param app_root App root directory.
2490 * @return Newly allocated absolute path, or NULL when it cannot be formed.
2491 */
2493 return app_root != NULL ? path_join(app_root, "data/framework/runtime-state-import-postgresql.sql") : NULL;
2494}
2495
2496/**
2497 * @brief Carries resolved PostgreSQL runtime connection settings for native relational bridge commands.
2498 */
2510
2511/**
2512 * @brief Releases owned PostgreSQL runtime connection settings.
2513 * @param config Runtime configuration to clear.
2514 */
2516 if (config == NULL) {
2517 return;
2518 }
2519 free(config->host);
2520 free(config->port);
2521 free(config->database);
2522 free(config->user);
2523 free(config->password_env_name);
2524 free(config->tls_mode);
2525 free(config->tls_root_cert);
2526 free(config->tls_cert);
2527 free(config->tls_key);
2528 memset(config, 0, sizeof(*config));
2529}
2530
2531/**
2532 * @brief Loads merged PostgreSQL runtime connection settings for one dynamic app.
2533 * @param app_root App root directory.
2534 * @param runtime_conf_path Runtime config path.
2535 * @param config Output configuration record.
2536 * @return Non-zero on success, otherwise 0.
2537 */
2538static int load_dynamic_postgresql_runtime_config(const char *app_root, const char *runtime_conf_path, DynamicPostgresqlRuntimeConfig *config) {
2539 if (app_root == NULL || app_root[0] == '\0' || config == NULL) {
2540 return 0;
2541 }
2542 memset(config, 0, sizeof(*config));
2543 config->host = dynamic_merged_config_value_runtime(app_root, runtime_conf_path, "db_host");
2544 config->port = dynamic_merged_config_value_runtime(app_root, runtime_conf_path, "db_port");
2545 config->database = dynamic_merged_config_value_runtime(app_root, runtime_conf_path, "db_name");
2546 config->user = dynamic_merged_config_value_runtime(app_root, runtime_conf_path, "db_user");
2547 config->password_env_name = dynamic_merged_config_value_runtime(app_root, runtime_conf_path, "db_password_env");
2548 config->tls_mode = dynamic_merged_config_value_runtime(app_root, runtime_conf_path, "db_tls_mode");
2549 config->tls_root_cert = dynamic_merged_config_value_runtime(app_root, runtime_conf_path, "db_tls_root_cert");
2550 config->tls_cert = dynamic_merged_config_value_runtime(app_root, runtime_conf_path, "db_tls_cert");
2551 config->tls_key = dynamic_merged_config_value_runtime(app_root, runtime_conf_path, "db_tls_key");
2552 if (config->tls_mode == NULL || config->tls_mode[0] == '\0') {
2553 free(config->tls_mode);
2554 config->tls_mode = strdup("require");
2555 }
2556 if (config->host == NULL || config->host[0] == '\0'
2557 || config->port == NULL || config->port[0] == '\0'
2558 || config->database == NULL || config->database[0] == '\0'
2559 || config->user == NULL || config->user[0] == '\0'
2560 || config->tls_mode == NULL || config->tls_mode[0] == '\0') {
2562 return 0;
2563 }
2564 if (!(streq(config->tls_mode, "disable") || streq(config->tls_mode, "allow") || streq(config->tls_mode, "prefer")
2565 || streq(config->tls_mode, "require") || streq(config->tls_mode, "verify-ca") || streq(config->tls_mode, "verify-full"))) {
2567 return 0;
2568 }
2569 if ((config->tls_root_cert != NULL && config->tls_root_cert[0] != '\0' && !path_exists(config->tls_root_cert))
2570 || (config->tls_cert != NULL && config->tls_cert[0] != '\0' && !path_exists(config->tls_cert))
2571 || (config->tls_key != NULL && config->tls_key[0] != '\0' && !path_exists(config->tls_key))) {
2573 return 0;
2574 }
2575 return 1;
2576}
2577
2578typedef struct pg_conn PGconn;
2579typedef struct pg_result PGresult;
2580
2581typedef struct {
2582#ifndef _WIN32
2583 void *handle;
2584#endif
2585 PGconn *(*PQconnectdb)(const char *conninfo);
2586 int (*PQstatus)(const PGconn *conn);
2587 char *(*PQerrorMessage)(const PGconn *conn);
2588 PGresult *(*PQexec)(PGconn *conn, const char *query);
2589 int (*PQresultStatus)(const PGresult *res);
2590 int (*PQntuples)(const PGresult *res);
2591 int (*PQnfields)(const PGresult *res);
2592 char *(*PQgetvalue)(const PGresult *res, int row_number, int column_number);
2593 void (*PQclear)(PGresult *res);
2594 void (*PQfinish)(PGconn *conn);
2596
2597#define PHAROS_CONN_STATUS_OK_RUNTIME 0
2598#define PHAROS_PGRES_COMMAND_OK_RUNTIME 1
2599#define PHAROS_PGRES_TUPLES_OK_RUNTIME 2
2600#define PHAROS_PGRES_SINGLE_TUPLE_RUNTIME 9
2601
2602/**
2603 * @brief Appends one libpq conninfo key or value fragment with native escaping.
2604 * @param buffer Destination text buffer.
2605 * @param text Fragment text.
2606 * @param escape_quotes Non-zero to escape backslashes and single quotes.
2607 * @return Non-zero on success, otherwise 0.
2608 */
2609static int conninfo_append_text_runtime(TextBufferRuntime *buffer, const char *text, int escape_quotes) {
2610 const char *cursor = text != NULL ? text : "";
2611 while (*cursor != '\0') {
2612 if (escape_quotes && (*cursor == '\\' || *cursor == '\'')) {
2613 if (!text_buffer_append_char_runtime(buffer, '\\')) {
2614 return 0;
2615 }
2616 }
2617 if (!text_buffer_append_char_runtime(buffer, *cursor)) {
2618 return 0;
2619 }
2620 cursor++;
2621 }
2622 return 1;
2623}
2624
2625/**
2626 * @brief Appends one libpq conninfo key value pair.
2627 * @param buffer Destination text buffer.
2628 * @param key Conninfo key name.
2629 * @param value Conninfo value.
2630 * @return Non-zero on success, otherwise 0.
2631 */
2632static int conninfo_append_kv_runtime(TextBufferRuntime *buffer, const char *key, const char *value) {
2633 if (buffer == NULL || key == NULL || key[0] == '\0' || value == NULL || value[0] == '\0') {
2634 return 0;
2635 }
2636 if (buffer->length > 0 && !text_buffer_append_char_runtime(buffer, ' ')) {
2637 return 0;
2638 }
2639 return conninfo_append_text_runtime(buffer, key, 0)
2640 && text_buffer_append_char_runtime(buffer, '=')
2641 && text_buffer_append_char_runtime(buffer, '\'')
2642 && conninfo_append_text_runtime(buffer, value, 1)
2643 && text_buffer_append_char_runtime(buffer, '\'');
2644}
2645
2646#ifndef _WIN32
2647/**
2648 * @brief Attempts to open one libpq shared library path.
2649 * @param path Candidate shared library path.
2650 * @return Dynamic loader handle, or NULL on failure.
2651 */
2652static void *open_libpq_handle_candidate_runtime(const char *path) {
2653 if (path == NULL || path[0] == '\0') {
2654 return NULL;
2655 }
2656 return dlopen(path, RTLD_NOW | RTLD_LOCAL);
2657}
2658#endif
2659
2660/**
2661 * @brief Loads the native libpq client ABI from cached image prefixes or standard library names.
2662 * @param api Receives resolved function pointers.
2663 * @return Non-zero on success, otherwise 0.
2664 */
2666#ifndef _WIN32
2667 static const char *const basenames[] = {"libpq.5.dylib", "libpq.dylib", "libpq.so.5", "libpq.so", NULL};
2668 const char *prefix = NULL;
2669 size_t index = 0;
2670#endif
2671 if (api == NULL) {
2672 return 0;
2673 }
2674 memset(api, 0, sizeof(*api));
2675#ifdef _WIN32
2676 return 0;
2677#else
2678 prefix = getenv("PHAROS_LIBPQ_PREFIX");
2679 if (prefix != NULL && prefix[0] != '\0') {
2680 char *lib_dir = path_join(prefix, "lib");
2681 if (lib_dir != NULL) {
2682 for (index = 0; basenames[index] != NULL && api->handle == NULL; index++) {
2683 char *candidate = path_join(lib_dir, basenames[index]);
2684 if (candidate != NULL && path_exists(candidate)) {
2686 }
2687 free(candidate);
2688 }
2689 free(lib_dir);
2690 }
2691 }
2692 for (index = 0; basenames[index] != NULL && api->handle == NULL; index++) {
2693 api->handle = open_libpq_handle_candidate_runtime(basenames[index]);
2694 }
2695 if (api->handle == NULL) {
2696 return 0;
2697 }
2698 api->PQconnectdb = (PGconn *(*)(const char *))dlsym(api->handle, "PQconnectdb");
2699 api->PQstatus = (int (*)(const PGconn *))dlsym(api->handle, "PQstatus");
2700 api->PQerrorMessage = (char *(*)(const PGconn *))dlsym(api->handle, "PQerrorMessage");
2701 api->PQexec = (PGresult *(*)(PGconn *, const char *))dlsym(api->handle, "PQexec");
2702 api->PQresultStatus = (int (*)(const PGresult *))dlsym(api->handle, "PQresultStatus");
2703 api->PQntuples = (int (*)(const PGresult *))dlsym(api->handle, "PQntuples");
2704 api->PQnfields = (int (*)(const PGresult *))dlsym(api->handle, "PQnfields");
2705 api->PQgetvalue = (char *(*)(const PGresult *, int, int))dlsym(api->handle, "PQgetvalue");
2706 api->PQclear = (void (*)(PGresult *))dlsym(api->handle, "PQclear");
2707 api->PQfinish = (void (*)(PGconn *))dlsym(api->handle, "PQfinish");
2708 if (api->PQconnectdb == NULL || api->PQstatus == NULL || api->PQerrorMessage == NULL || api->PQexec == NULL
2709 || api->PQresultStatus == NULL || api->PQntuples == NULL || api->PQnfields == NULL || api->PQgetvalue == NULL
2710 || api->PQclear == NULL || api->PQfinish == NULL) {
2711 dlclose(api->handle);
2712 memset(api, 0, sizeof(*api));
2713 return 0;
2714 }
2715 return 1;
2716#endif
2717}
2718
2719/**
2720 * @brief Releases one resolved native libpq ABI loader handle.
2721 * @param api Loaded libpq function table.
2722 */
2724#ifndef _WIN32
2725 if (api != NULL && api->handle != NULL) {
2726 dlclose(api->handle);
2727 }
2728#endif
2729 if (api != NULL) {
2730 memset(api, 0, sizeof(*api));
2731 }
2732}
2733
2734/**
2735 * @brief Builds one libpq conninfo string from merged runtime config values.
2736 * @param config Resolved PostgreSQL runtime connection settings.
2737 * @return Newly allocated conninfo string, or NULL on failure.
2738 */
2740 TextBufferRuntime buffer = {0};
2741 const char *password_value = NULL;
2742 if (config == NULL) {
2743 return NULL;
2744 }
2745 if (!conninfo_append_kv_runtime(&buffer, "host", config->host)
2746 || !conninfo_append_kv_runtime(&buffer, "port", config->port)
2747 || !conninfo_append_kv_runtime(&buffer, "dbname", config->database)
2748 || !conninfo_append_kv_runtime(&buffer, "user", config->user)
2749 || !conninfo_append_kv_runtime(&buffer, "sslmode", config->tls_mode)) {
2750 text_buffer_free_runtime(&buffer);
2751 return NULL;
2752 }
2753 if (config->password_env_name != NULL && config->password_env_name[0] != '\0') {
2754 password_value = getenv(config->password_env_name);
2755 if (password_value != NULL && password_value[0] != '\0' && !conninfo_append_kv_runtime(&buffer, "password", password_value)) {
2756 text_buffer_free_runtime(&buffer);
2757 return NULL;
2758 }
2759 }
2760 if ((config->tls_root_cert != NULL && config->tls_root_cert[0] != '\0' && !conninfo_append_kv_runtime(&buffer, "sslrootcert", config->tls_root_cert))
2761 || (config->tls_cert != NULL && config->tls_cert[0] != '\0' && !conninfo_append_kv_runtime(&buffer, "sslcert", config->tls_cert))
2762 || (config->tls_key != NULL && config->tls_key[0] != '\0' && !conninfo_append_kv_runtime(&buffer, "sslkey", config->tls_key))) {
2763 text_buffer_free_runtime(&buffer);
2764 return NULL;
2765 }
2766 return text_buffer_take_runtime(&buffer);
2767}
2768
2769/**
2770 * @brief Captures one tuple-producing PostgreSQL result into CLI-compatible line output.
2771 * @param api Loaded libpq function table.
2772 * @param result Query result to serialize.
2773 * @param output_out Receives owned serialized text.
2774 * @return Native status code.
2775 */
2776static int capture_postgresql_result_output_runtime(const NativeLibpqApi *api, PGresult *result, char **output_out) {
2777 TextBufferRuntime buffer = {0};
2778 int row_count = 0;
2779 int column_count = 0;
2780 int row_index = 0;
2781 int column_index = 0;
2782 if (output_out != NULL) {
2783 *output_out = NULL;
2784 }
2785 if (api == NULL || result == NULL || output_out == NULL) {
2786 return 64;
2787 }
2788 row_count = api->PQntuples(result);
2789 column_count = api->PQnfields(result);
2790 for (row_index = 0; row_index < row_count; row_index++) {
2791 if (row_index > 0 && !text_buffer_append_char_runtime(&buffer, '\n')) {
2792 text_buffer_free_runtime(&buffer);
2793 return 69;
2794 }
2795 for (column_index = 0; column_index < column_count; column_index++) {
2796 const char *value = api->PQgetvalue(result, row_index, column_index);
2797 if (column_index > 0 && !text_buffer_append_char_runtime(&buffer, '|')) {
2798 text_buffer_free_runtime(&buffer);
2799 return 69;
2800 }
2801 if (value != NULL && !text_buffer_append_text_runtime(&buffer, value)) {
2802 text_buffer_free_runtime(&buffer);
2803 return 69;
2804 }
2805 }
2806 }
2807 *output_out = text_buffer_take_runtime(&buffer);
2808 if (*output_out == NULL) {
2809 return 69;
2810 }
2811 trim_in_place(*output_out);
2812 return 0;
2813}
2814
2815/**
2816 * @brief Runs one PostgreSQL SQL text payload through the native libpq driver.
2817 * @param config Resolved PostgreSQL runtime connection settings.
2818 * @param sql_text SQL text to execute.
2819 * @param at_output Non-zero to request tuple capture output.
2820 * @param output_out Receives captured output when requested.
2821 * @return Native status code.
2822 */
2823static int run_postgresql_sql_capture_runtime(const DynamicPostgresqlRuntimeConfig *config, const char *sql_text, int at_output, char **output_out) {
2824 NativeLibpqApi api = {0};
2825 PGconn *connection = NULL;
2826 PGresult *result = NULL;
2827 char *conninfo = NULL;
2828 int status = 0;
2829 int code = 69;
2830 if (output_out != NULL) {
2831 *output_out = NULL;
2832 }
2833 if (config == NULL || sql_text == NULL) {
2834 return 64;
2835 }
2836 if (!load_native_libpq_api_runtime(&api)) {
2837 return 69;
2838 }
2839 conninfo = postgresql_conninfo_dup_runtime(config);
2840 connection = conninfo != NULL ? api.PQconnectdb(conninfo) : NULL;
2841 if (connection == NULL || api.PQstatus(connection) != PHAROS_CONN_STATUS_OK_RUNTIME) {
2842 goto cleanup;
2843 }
2844 result = api.PQexec(connection, sql_text);
2845 if (result == NULL) {
2846 goto cleanup;
2847 }
2848 status = api.PQresultStatus(result);
2849 if (at_output) {
2851 goto cleanup;
2852 }
2853 code = capture_postgresql_result_output_runtime(&api, result, output_out);
2855 code = 0;
2856 }
2857cleanup:
2858 if (result != NULL) {
2859 api.PQclear(result);
2860 }
2861 if (connection != NULL) {
2862 api.PQfinish(connection);
2863 }
2864 free(conninfo);
2866 return code;
2867}
2868
2869/**
2870 * @brief Replaces the canonical runtime-state placeholder inside one declarative SQL template.
2871 * @param sql_template Declarative SQL template text.
2872 * @param escaped_state_json SQL-literal escaped canonical state JSON.
2873 * @return Newly allocated SQL text, or NULL on failure.
2874 */
2875static char *runtime_state_placeholder_sql_dup_runtime(const char *sql_template, const char *escaped_state_json) {
2876 const char *placeholder = NULL;
2877 size_t prefix_len = 0;
2878 size_t suffix_len = 0;
2879 char *sql_text = NULL;
2880 if (sql_template == NULL || escaped_state_json == NULL) {
2881 return NULL;
2882 }
2883 placeholder = strstr(sql_template, "__STATE_JSON__");
2884 if (placeholder == NULL) {
2885 return NULL;
2886 }
2887 prefix_len = (size_t)(placeholder - sql_template);
2888 suffix_len = strlen(placeholder + strlen("__STATE_JSON__"));
2889 sql_text = (char *)malloc(prefix_len + strlen(escaped_state_json) + suffix_len + 1);
2890 if (sql_text == NULL) {
2891 return NULL;
2892 }
2893 memcpy(sql_text, sql_template, prefix_len);
2894 memcpy(sql_text + prefix_len, escaped_state_json, strlen(escaped_state_json));
2895 memcpy(sql_text + prefix_len + strlen(escaped_state_json), placeholder + strlen("__STATE_JSON__"), suffix_len + 1);
2896 return sql_text;
2897}
2898
2899/**
2900 * @brief Exports UCAL relational runtime state into canonical JSON using the configured backend contract.
2901 * @param app_root App root directory.
2902 * @param runtime_conf_path Runtime config path.
2903 * @param app_id Dynamic app identifier.
2904 * @return Newly allocated compact state JSON, or NULL on failure.
2905 */
2906static char *dynamic_relational_runtime_export_json_dup_runtime(const char *app_root, const char *runtime_conf_path, const char *app_id) {
2907 char *backend = NULL;
2908 char *sqlite_path = NULL;
2909 char *sql_path = NULL;
2910 char *sql_text = NULL;
2911 char *export_json = NULL;
2913 if (app_root == NULL || app_root[0] == '\0') {
2914 return NULL;
2915 }
2916 backend = dynamic_merged_config_value_runtime(app_root, runtime_conf_path, "db_backend");
2917 if (backend == NULL || backend[0] == '\0') {
2918 free(backend);
2919 backend = strdup("sqlite");
2920 }
2921 if (backend == NULL) {
2922 return NULL;
2923 }
2924 if (streq(backend, "sqlite")) {
2925 sqlite_path = dynamic_sqlite_path_dup_runtime(app_root, runtime_conf_path, app_id);
2927 sql_text = sql_path != NULL ? read_file_text(sql_path) : NULL;
2928 if (sqlite_path == NULL || sqlite_path[0] == '\0' || sql_text == NULL || run_sqlite_command_runtime(sqlite_path, sql_text, &export_json) != 0) {
2929 free(export_json);
2930 export_json = NULL;
2931 }
2932 } else if (streq(backend, "postgresql")) {
2933 if (load_dynamic_postgresql_runtime_config(app_root, runtime_conf_path, &pg)) {
2935 sql_text = sql_path != NULL ? read_file_text(sql_path) : NULL;
2936 if (sql_text == NULL || run_postgresql_sql_capture_runtime(&pg, sql_text, 1, &export_json) != 0) {
2937 free(export_json);
2938 export_json = NULL;
2939 }
2940 }
2941 }
2942 free(backend);
2943 free(sqlite_path);
2944 free(sql_path);
2945 free(sql_text);
2947 return export_json;
2948}
2949
2950/**
2951 * @brief Returns the current audit-event count for one dynamic relational runtime using the configured backend contract.
2952 * @param app_root App root directory.
2953 * @param runtime_conf_path Runtime config path.
2954 * @param app_id Dynamic app identifier.
2955 * @return Newly allocated count text, or NULL on failure.
2956 */
2957static char *dynamic_relational_runtime_audit_count_dup_runtime(const char *app_root, const char *runtime_conf_path, const char *app_id) {
2958 char *backend = NULL;
2959 char *sqlite_path = NULL;
2960 char *table_present = NULL;
2961 char *count_text = NULL;
2963 if (app_root == NULL || app_root[0] == '\0') {
2964 return NULL;
2965 }
2966 backend = dynamic_merged_config_value_runtime(app_root, runtime_conf_path, "db_backend");
2967 if (backend == NULL || backend[0] == '\0') {
2968 free(backend);
2969 backend = strdup("sqlite");
2970 }
2971 if (backend == NULL) {
2972 return NULL;
2973 }
2974 if (streq(backend, "sqlite")) {
2975 sqlite_path = dynamic_sqlite_path_dup_runtime(app_root, runtime_conf_path, app_id);
2976 if (sqlite_path != NULL && sqlite_path[0] != '\0') {
2977 table_present = sqlite_query_value_runtime(sqlite_path, "SELECT name FROM sqlite_master WHERE type='table' AND name='audit_event';");
2978 if (table_present == NULL || table_present[0] == '\0') {
2979 count_text = strdup("0");
2980 } else {
2981 count_text = sqlite_query_value_runtime(sqlite_path, "SELECT COUNT(*) FROM audit_event;");
2982 }
2983 }
2984 } else if (streq(backend, "postgresql")) {
2985 if (load_dynamic_postgresql_runtime_config(app_root, runtime_conf_path, &pg)) {
2986 char *table_exists = NULL;
2987 if (run_postgresql_sql_capture_runtime(&pg, "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'audit_event');", 1, &table_exists) == 0) {
2988 if (table_exists != NULL && streq(table_exists, "t")) {
2989 count_text = NULL;
2990 run_postgresql_sql_capture_runtime(&pg, "SELECT COUNT(*) FROM audit_event;", 1, &count_text);
2991 } else {
2992 count_text = strdup("0");
2993 }
2994 }
2995 free(table_exists);
2996 }
2997 }
2998 free(backend);
2999 free(sqlite_path);
3000 free(table_present);
3002 return count_text;
3003}
3004
3005/**
3006 * @brief Imports canonical UCAL relational runtime state using the configured backend contract.
3007 * @param app_root App root directory.
3008 * @param runtime_conf_path Runtime config path.
3009 * @param app_id Dynamic app identifier.
3010 * @param state_path Canonical state JSON path.
3011 * @param expected_audit_count Optional optimistic concurrency guard.
3012 * @return Native status code.
3013 */
3014static int dynamic_relational_runtime_import_state_runtime(const char *app_root, const char *runtime_conf_path, const char *app_id, const char *state_path, const char *expected_audit_count) {
3015 char *backend = NULL;
3016 char *sqlite_path = NULL;
3017 char *current_audit_count = NULL;
3018 char *state_json = NULL;
3019 char *escaped_state_json = NULL;
3020 char *sql_template_path = NULL;
3021 char *sql_template = NULL;
3022 char *sql_text = NULL;
3024 int code = 69;
3025 if (app_root == NULL || app_root[0] == '\0' || state_path == NULL || state_path[0] == '\0') {
3026 return 64;
3027 }
3028 backend = dynamic_merged_config_value_runtime(app_root, runtime_conf_path, "db_backend");
3029 if (backend == NULL || backend[0] == '\0') {
3030 free(backend);
3031 backend = strdup("sqlite");
3032 }
3033 if (backend == NULL) {
3034 return 69;
3035 }
3036 if (expected_audit_count != NULL && expected_audit_count[0] != '\0') {
3037 current_audit_count = dynamic_relational_runtime_audit_count_dup_runtime(app_root, runtime_conf_path, app_id);
3038 if (current_audit_count == NULL || !streq(current_audit_count, expected_audit_count)) {
3039 code = 73;
3040 goto dynamic_relational_runtime_import_cleanup;
3041 }
3042 }
3043 state_json = read_file_text(state_path);
3044 escaped_state_json = sql_literal_escape_dup_runtime(state_json != NULL ? state_json : "");
3045 if (streq(backend, "sqlite")) {
3046 sqlite_path = dynamic_sqlite_path_dup_runtime(app_root, runtime_conf_path, app_id);
3047 sql_template_path = ucal_runtime_state_import_sqlite_path_dup_runtime(app_root);
3048 sql_template = sql_template_path != NULL ? read_file_text(sql_template_path) : NULL;
3049 sql_text = runtime_state_placeholder_sql_dup_runtime(sql_template, escaped_state_json != NULL ? escaped_state_json : "");
3050 if (sqlite_path == NULL || sqlite_path[0] == '\0' || sql_text == NULL) {
3051 goto dynamic_relational_runtime_import_cleanup;
3052 }
3053 code = run_sqlite_command_runtime(sqlite_path, sql_text, NULL);
3054 } else if (streq(backend, "postgresql")) {
3055 if (!load_dynamic_postgresql_runtime_config(app_root, runtime_conf_path, &pg)) {
3056 goto dynamic_relational_runtime_import_cleanup;
3057 }
3058 sql_template_path = ucal_runtime_state_import_postgresql_path_dup_runtime(app_root);
3059 sql_template = sql_template_path != NULL ? read_file_text(sql_template_path) : NULL;
3060 sql_text = runtime_state_placeholder_sql_dup_runtime(sql_template, escaped_state_json != NULL ? escaped_state_json : "");
3061 if (sql_text == NULL) {
3062 goto dynamic_relational_runtime_import_cleanup;
3063 }
3064 code = run_postgresql_sql_capture_runtime(&pg, sql_text, 0, NULL);
3065 }
3066 dynamic_relational_runtime_import_cleanup:
3067 free(backend);
3068 free(sqlite_path);
3069 free(current_audit_count);
3070 free(state_json);
3071 free(escaped_state_json);
3072 free(sql_template_path);
3073 free(sql_template);
3074 free(sql_text);
3076 return code;
3077}
3078
3079/**
3080 * @brief Tests whether one identifier contains only letters, digits, and underscores.
3081 * @param text Candidate identifier text.
3082 * @return Non-zero when the identifier is safe for quoted SQL identifier use.
3083 */
3084static int identifier_safe_runtime(const char *text) {
3085 const unsigned char *cursor = (const unsigned char *)text;
3086 if (text == NULL || text[0] == '\0') {
3087 return 0;
3088 }
3089 while (*cursor != '\0') {
3090 if (!(isalnum(*cursor) || *cursor == '_')) {
3091 return 0;
3092 }
3093 cursor++;
3094 }
3095 return 1;
3096}
3097
3098/**
3099 * @brief Loads PostgreSQL connection settings from explicit JSON command options.
3100 * @param argc Command argument count.
3101 * @param argv Command argument vector.
3102 * @param config Receives the owned configuration record.
3103 * @return Non-zero on success, otherwise 0.
3104 */
3105static int load_cli_postgresql_runtime_config(int argc, char **argv, DynamicPostgresqlRuntimeConfig *config) {
3106 const char *host = json_option_value_runtime(argc, argv, "--host");
3107 const char *port = json_option_value_runtime(argc, argv, "--port");
3108 const char *database = json_option_value_runtime(argc, argv, "--database");
3109 const char *user = json_option_value_runtime(argc, argv, "--user");
3110 const char *password_env = json_option_value_runtime(argc, argv, "--password-env");
3111 const char *tls_mode = json_option_value_runtime(argc, argv, "--tls-mode");
3112 const char *tls_root_cert = json_option_value_runtime(argc, argv, "--tls-root-cert");
3113 const char *tls_cert = json_option_value_runtime(argc, argv, "--tls-cert");
3114 const char *tls_key = json_option_value_runtime(argc, argv, "--tls-key");
3115 if (config == NULL || host == NULL || host[0] == '\0' || port == NULL || port[0] == '\0'
3116 || database == NULL || database[0] == '\0' || user == NULL || user[0] == '\0') {
3117 return 0;
3118 }
3119 memset(config, 0, sizeof(*config));
3120 config->host = strdup(host);
3121 config->port = strdup(port);
3122 config->database = strdup(database);
3123 config->user = strdup(user);
3124 config->password_env_name = password_env != NULL && password_env[0] != '\0' ? strdup(password_env) : strdup("");
3125 config->tls_mode = tls_mode != NULL && tls_mode[0] != '\0' ? strdup(tls_mode) : strdup("require");
3126 config->tls_root_cert = tls_root_cert != NULL && tls_root_cert[0] != '\0' ? strdup(tls_root_cert) : strdup("");
3127 config->tls_cert = tls_cert != NULL && tls_cert[0] != '\0' ? strdup(tls_cert) : strdup("");
3128 config->tls_key = tls_key != NULL && tls_key[0] != '\0' ? strdup(tls_key) : strdup("");
3129 if (config->host == NULL || config->port == NULL || config->database == NULL || config->user == NULL
3130 || config->password_env_name == NULL || config->tls_mode == NULL || config->tls_root_cert == NULL
3131 || config->tls_cert == NULL || config->tls_key == NULL) {
3133 return 0;
3134 }
3135 if (!(streq(config->tls_mode, "disable") || streq(config->tls_mode, "allow") || streq(config->tls_mode, "prefer")
3136 || streq(config->tls_mode, "require") || streq(config->tls_mode, "verify-ca") || streq(config->tls_mode, "verify-full"))) {
3138 return 0;
3139 }
3140 if ((config->tls_root_cert[0] != '\0' && !path_exists(config->tls_root_cert))
3141 || (config->tls_cert[0] != '\0' && !path_exists(config->tls_cert))
3142 || (config->tls_key[0] != '\0' && !path_exists(config->tls_key))) {
3144 return 0;
3145 }
3146 return 1;
3147}
3148
3149/**
3150 * @brief Duplicates one PostgreSQL runtime configuration while replacing the database name.
3151 * @param source Source configuration.
3152 * @param database_name Replacement database name.
3153 * @param copy_out Receives the duplicated configuration.
3154 * @return Non-zero on success, otherwise 0.
3155 */
3157 if (source == NULL || database_name == NULL || database_name[0] == '\0' || copy_out == NULL) {
3158 return 0;
3159 }
3160 memset(copy_out, 0, sizeof(*copy_out));
3161 copy_out->host = source->host != NULL ? strdup(source->host) : strdup("");
3162 copy_out->port = source->port != NULL ? strdup(source->port) : strdup("");
3163 copy_out->database = strdup(database_name);
3164 copy_out->user = source->user != NULL ? strdup(source->user) : strdup("");
3165 copy_out->password_env_name = source->password_env_name != NULL ? strdup(source->password_env_name) : strdup("");
3166 copy_out->tls_mode = source->tls_mode != NULL ? strdup(source->tls_mode) : strdup("require");
3167 copy_out->tls_root_cert = source->tls_root_cert != NULL ? strdup(source->tls_root_cert) : strdup("");
3168 copy_out->tls_cert = source->tls_cert != NULL ? strdup(source->tls_cert) : strdup("");
3169 copy_out->tls_key = source->tls_key != NULL ? strdup(source->tls_key) : strdup("");
3170 if (copy_out->host == NULL || copy_out->port == NULL || copy_out->database == NULL || copy_out->user == NULL
3171 || copy_out->password_env_name == NULL || copy_out->tls_mode == NULL || copy_out->tls_root_cert == NULL
3172 || copy_out->tls_cert == NULL || copy_out->tls_key == NULL) {
3174 return 0;
3175 }
3176 return 1;
3177}
3178
3179/**
3180 * @brief Emits one digit boolean result for shell callers.
3181 * @param truthy Non-zero when the answer is true.
3182 * @return Native status code.
3183 */
3184static int emit_digit_bool_runtime(int truthy) {
3185 fputs(truthy ? "1" : "0", stdout);
3186 return 0;
3187}
3188
3189/**
3190 * @brief Emits whether one sqlite table exists.
3191 * @param argc Command argument count.
3192 * @param argv Command argument vector.
3193 * @return Native status code.
3194 */
3195static int handle_db_sqlite_table_exists_command(int argc, char **argv) {
3196 const char *db_path = json_option_value_runtime(argc, argv, "--db-path");
3197 const char *table_name = json_option_value_runtime(argc, argv, "--table");
3198 char *escaped_table = NULL;
3199 char query[2048];
3200 char *value = NULL;
3201 int exists = 0;
3202 if (db_path == NULL || db_path[0] == '\0' || !identifier_safe_runtime(table_name)) {
3203 return 64;
3204 }
3205 escaped_table = sql_literal_escape_dup_runtime(table_name);
3206 if (escaped_table == NULL) {
3207 return 69;
3208 }
3209 snprintf(query, sizeof(query), "SELECT name FROM sqlite_master WHERE type = 'table' AND name = '%s' LIMIT 1;", escaped_table);
3210 value = sqlite_query_value_runtime(db_path, query);
3211 exists = value != NULL && streq(value, table_name);
3212 free(escaped_table);
3213 free(value);
3214 return emit_digit_bool_runtime(exists);
3215}
3216
3217/**
3218 * @brief Emits one sqlite table row count.
3219 * @param argc Command argument count.
3220 * @param argv Command argument vector.
3221 * @return Native status code.
3222 */
3223static int handle_db_sqlite_table_row_count_command(int argc, char **argv) {
3224 const char *db_path = json_option_value_runtime(argc, argv, "--db-path");
3225 const char *table_name = json_option_value_runtime(argc, argv, "--table");
3226 char sql[1024];
3227 char *value = NULL;
3228 if (db_path == NULL || db_path[0] == '\0' || !identifier_safe_runtime(table_name)) {
3229 return 64;
3230 }
3231 snprintf(sql, sizeof(sql), "SELECT COUNT(*) FROM \"%s\";", table_name);
3232 value = sqlite_query_value_runtime(db_path, sql);
3233 fputs(value != NULL ? value : "0", stdout);
3234 free(value);
3235 return 0;
3236}
3237
3238/**
3239 * @brief Emits one sqlite non-system table count.
3240 * @param argc Command argument count.
3241 * @param argv Command argument vector.
3242 * @return Native status code.
3243 */
3244static int handle_db_sqlite_table_count_command(int argc, char **argv) {
3245 const char *db_path = json_option_value_runtime(argc, argv, "--db-path");
3246 char *value = NULL;
3247 if (db_path == NULL || db_path[0] == '\0') {
3248 return 64;
3249 }
3250 value = sqlite_query_value_runtime(db_path, "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%';");
3251 fputs(value != NULL ? value : "0", stdout);
3252 free(value);
3253 return 0;
3254}
3255
3256/**
3257 * @brief Emits one sqlite migration applied count.
3258 * @param argc Command argument count.
3259 * @param argv Command argument vector.
3260 * @return Native status code.
3261 */
3262static int handle_db_sqlite_migration_applied_count_command(int argc, char **argv) {
3263 const char *db_path = json_option_value_runtime(argc, argv, "--db-path");
3264 const char *migration_id = json_option_value_runtime(argc, argv, "--migration-id");
3265 const char *backend = json_option_value_runtime(argc, argv, "--backend");
3266 char *escaped_id = NULL;
3267 char *escaped_backend = NULL;
3268 char query[2048];
3269 char *value = NULL;
3270 if (db_path == NULL || db_path[0] == '\0' || migration_id == NULL || backend == NULL) {
3271 return 64;
3272 }
3273 escaped_id = sql_literal_escape_dup_runtime(migration_id);
3274 escaped_backend = sql_literal_escape_dup_runtime(backend);
3275 if (escaped_id == NULL || escaped_backend == NULL) {
3276 free(escaped_id);
3277 free(escaped_backend);
3278 return 69;
3279 }
3280 snprintf(query, sizeof(query), "SELECT COUNT(*) FROM pharos_schema_migration WHERE migration_id = '%s' AND backend = '%s';", escaped_id, escaped_backend);
3281 value = sqlite_query_value_runtime(db_path, query);
3282 fputs(value != NULL ? value : "0", stdout);
3283 free(escaped_id);
3284 free(escaped_backend);
3285 free(value);
3286 return 0;
3287}
3288
3289/**
3290 * @brief Emits one sqlite migration applied schema version.
3291 * @param argc Command argument count.
3292 * @param argv Command argument vector.
3293 * @return Native status code.
3294 */
3296 const char *db_path = json_option_value_runtime(argc, argv, "--db-path");
3297 const char *migration_id = json_option_value_runtime(argc, argv, "--migration-id");
3298 const char *backend = json_option_value_runtime(argc, argv, "--backend");
3299 char *escaped_id = NULL;
3300 char *escaped_backend = NULL;
3301 char query[2048];
3302 char *value = NULL;
3303 if (db_path == NULL || db_path[0] == '\0' || migration_id == NULL || backend == NULL) {
3304 return 64;
3305 }
3306 escaped_id = sql_literal_escape_dup_runtime(migration_id);
3307 escaped_backend = sql_literal_escape_dup_runtime(backend);
3308 if (escaped_id == NULL || escaped_backend == NULL) {
3309 free(escaped_id);
3310 free(escaped_backend);
3311 return 69;
3312 }
3313 snprintf(query, sizeof(query), "SELECT schema_version FROM pharos_schema_migration WHERE migration_id = '%s' AND backend = '%s' ORDER BY applied_at DESC LIMIT 1;", escaped_id, escaped_backend);
3314 value = sqlite_query_value_runtime(db_path, query);
3315 fputs(value != NULL ? value : "", stdout);
3316 free(escaped_id);
3317 free(escaped_backend);
3318 free(value);
3319 return 0;
3320}
3321
3322/**
3323 * @brief Emits one sqlite migration applied checksum.
3324 * @param argc Command argument count.
3325 * @param argv Command argument vector.
3326 * @return Native status code.
3327 */
3329 const char *db_path = json_option_value_runtime(argc, argv, "--db-path");
3330 const char *migration_id = json_option_value_runtime(argc, argv, "--migration-id");
3331 const char *backend = json_option_value_runtime(argc, argv, "--backend");
3332 char *escaped_id = NULL;
3333 char *escaped_backend = NULL;
3334 char query[2048];
3335 char *value = NULL;
3336 if (db_path == NULL || db_path[0] == '\0' || migration_id == NULL || backend == NULL) {
3337 return 64;
3338 }
3339 escaped_id = sql_literal_escape_dup_runtime(migration_id);
3340 escaped_backend = sql_literal_escape_dup_runtime(backend);
3341 if (escaped_id == NULL || escaped_backend == NULL) {
3342 free(escaped_id);
3343 free(escaped_backend);
3344 return 69;
3345 }
3346 snprintf(query, sizeof(query), "SELECT checksum_sha256 FROM pharos_schema_migration WHERE migration_id = '%s' AND backend = '%s' ORDER BY applied_at DESC LIMIT 1;", escaped_id, escaped_backend);
3347 value = sqlite_query_value_runtime(db_path, query);
3348 fputs(value != NULL ? value : "", stdout);
3349 free(escaped_id);
3350 free(escaped_backend);
3351 free(value);
3352 return 0;
3353}
3354
3355/**
3356 * @brief Updates a sqlite migration checksum when the ledger row is missing one.
3357 * @param argc Command argument count.
3358 * @param argv Command argument vector.
3359 * @return Native status code.
3360 */
3362 const char *db_path = json_option_value_runtime(argc, argv, "--db-path");
3363 const char *migration_id = json_option_value_runtime(argc, argv, "--migration-id");
3364 const char *backend = json_option_value_runtime(argc, argv, "--backend");
3365 const char *checksum = json_option_value_runtime(argc, argv, "--checksum");
3366 char *escaped_id = NULL;
3367 char *escaped_backend = NULL;
3368 char *escaped_checksum = NULL;
3369 char query[4096];
3370 if (db_path == NULL || db_path[0] == '\0' || migration_id == NULL || backend == NULL || checksum == NULL) {
3371 return 64;
3372 }
3373 escaped_id = sql_literal_escape_dup_runtime(migration_id);
3374 escaped_backend = sql_literal_escape_dup_runtime(backend);
3375 escaped_checksum = sql_literal_escape_dup_runtime(checksum);
3376 if (escaped_id == NULL || escaped_backend == NULL || escaped_checksum == NULL) {
3377 free(escaped_id);
3378 free(escaped_backend);
3379 free(escaped_checksum);
3380 return 69;
3381 }
3382 snprintf(query, sizeof(query), "UPDATE pharos_schema_migration SET checksum_sha256 = '%s' WHERE migration_id = '%s' AND backend = '%s' AND (checksum_sha256 IS NULL OR TRIM(checksum_sha256) = '');", escaped_checksum, escaped_id, escaped_backend);
3383 free(escaped_id);
3384 free(escaped_backend);
3385 free(escaped_checksum);
3386 return run_sqlite_command_runtime(db_path, query, NULL);
3387}
3388
3389/**
3390 * @brief Inserts one sqlite migration ledger record.
3391 * @param argc Command argument count.
3392 * @param argv Command argument vector.
3393 * @return Native status code.
3394 */
3395static int handle_db_sqlite_record_migration_command(int argc, char **argv) {
3396 const char *db_path = json_option_value_runtime(argc, argv, "--db-path");
3397 const char *migration_id = json_option_value_runtime(argc, argv, "--migration-id");
3398 const char *schema_version = json_option_value_runtime(argc, argv, "--schema-version");
3399 const char *backend = json_option_value_runtime(argc, argv, "--backend");
3400 const char *checksum = json_option_value_runtime(argc, argv, "--checksum");
3401 char *escaped_id = NULL;
3402 char *escaped_backend = NULL;
3403 char *escaped_checksum = NULL;
3404 char query[4096];
3405 if (db_path == NULL || db_path[0] == '\0' || migration_id == NULL || schema_version == NULL || backend == NULL || checksum == NULL) {
3406 return 64;
3407 }
3408 escaped_id = sql_literal_escape_dup_runtime(migration_id);
3409 escaped_backend = sql_literal_escape_dup_runtime(backend);
3410 escaped_checksum = sql_literal_escape_dup_runtime(checksum);
3411 if (escaped_id == NULL || escaped_backend == NULL || escaped_checksum == NULL) {
3412 free(escaped_id);
3413 free(escaped_backend);
3414 free(escaped_checksum);
3415 return 69;
3416 }
3417 snprintf(query, sizeof(query), "INSERT INTO pharos_schema_migration (migration_id, schema_version, backend, checksum_sha256) VALUES ('%s', %s, '%s', '%s');", escaped_id, schema_version, escaped_backend, escaped_checksum);
3418 free(escaped_id);
3419 free(escaped_backend);
3420 free(escaped_checksum);
3421 return run_sqlite_command_runtime(db_path, query, NULL);
3422}
3423
3424/**
3425 * @brief Emits the current sqlite audit event count.
3426 * @param argc Command argument count.
3427 * @param argv Command argument vector.
3428 * @return Native status code.
3429 */
3430static int handle_db_sqlite_audit_event_count_command(int argc, char **argv) {
3431 const char *db_path = json_option_value_runtime(argc, argv, "--db-path");
3432 char *table_present = NULL;
3433 char *count_text = NULL;
3434 if (db_path == NULL || db_path[0] == '\0') {
3435 return 64;
3436 }
3437 table_present = sqlite_query_value_runtime(db_path, "SELECT name FROM sqlite_master WHERE type='table' AND name='audit_event';");
3438 if (table_present == NULL || table_present[0] == '\0') {
3439 fputs("0", stdout);
3440 } else {
3441 count_text = sqlite_query_value_runtime(db_path, "SELECT COUNT(*) FROM audit_event;");
3442 fputs(count_text != NULL ? count_text : "0", stdout);
3443 }
3444 free(table_present);
3445 free(count_text);
3446 return 0;
3447}
3448
3449/**
3450 * @brief Emits the first-line output of one sqlite query.
3451 * @param argc Command argument count.
3452 * @param argv Command argument vector.
3453 * @return Native status code.
3454 */
3455static int handle_db_sqlite_query_value_command(int argc, char **argv) {
3456 const char *db_path = json_option_value_runtime(argc, argv, "--db-path");
3457 const char *sql = json_option_value_runtime(argc, argv, "--sql");
3458 char *value = NULL;
3459 if (db_path == NULL || db_path[0] == '\0' || sql == NULL) {
3460 return 64;
3461 }
3462 value = sqlite_query_value_runtime(db_path, sql);
3463 if (value != NULL) {
3464 fputs(value, stdout);
3465 }
3466 free(value);
3467 return 0;
3468}
3469
3470/**
3471 * @brief Executes one sqlite SQL text payload.
3472 * @param argc Command argument count.
3473 * @param argv Command argument vector.
3474 * @return Native status code.
3475 */
3476static int handle_db_sqlite_exec_command(int argc, char **argv) {
3477 const char *db_path = json_option_value_runtime(argc, argv, "--db-path");
3478 const char *sql = json_option_value_runtime(argc, argv, "--sql");
3479 if (db_path == NULL || db_path[0] == '\0' || sql == NULL) {
3480 return 64;
3481 }
3482 return run_sqlite_command_runtime(db_path, sql, NULL);
3483}
3484
3485/**
3486 * @brief Executes one sqlite SQL file.
3487 * @param argc Command argument count.
3488 * @param argv Command argument vector.
3489 * @return Native status code.
3490 */
3491static int handle_db_sqlite_exec_file_command(int argc, char **argv) {
3492 const char *db_path = json_option_value_runtime(argc, argv, "--db-path");
3493 const char *file_path = json_option_value_runtime(argc, argv, "--file");
3494 char *sql = NULL;
3495 int code = 64;
3496 if (db_path == NULL || db_path[0] == '\0' || file_path == NULL || file_path[0] == '\0') {
3497 return 64;
3498 }
3499 sql = read_file_text(file_path);
3500 if (sql == NULL) {
3501 return 66;
3502 }
3503 code = run_sqlite_command_runtime(db_path, sql, NULL);
3504 free(sql);
3505 return code;
3506}
3507
3508/**
3509 * @brief Emits whether one PostgreSQL database exists.
3510 * @param argc Command argument count.
3511 * @param argv Command argument vector.
3512 * @return Native status code.
3513 */
3514static int handle_db_postgresql_database_exists_command(int argc, char **argv) {
3515 const char *database_name = json_option_value_runtime(argc, argv, "--database");
3516 DynamicPostgresqlRuntimeConfig config = {0};
3518 char *escaped_database = NULL;
3519 char query[2048];
3520 char *value = NULL;
3521 int code = 64;
3522 if (database_name == NULL || database_name[0] == '\0' || !identifier_safe_runtime(database_name)) {
3523 return 64;
3524 }
3525 if (!load_cli_postgresql_runtime_config(argc, argv, &config) || !duplicate_postgresql_runtime_config_with_database_runtime(&config, "postgres", &admin)) {
3526 code = 69;
3527 goto cleanup;
3528 }
3529 escaped_database = sql_literal_escape_dup_runtime(database_name);
3530 if (escaped_database == NULL) {
3531 code = 69;
3532 goto cleanup;
3533 }
3534 snprintf(query, sizeof(query), "SELECT 1 FROM pg_database WHERE datname = '%s' LIMIT 1;", escaped_database);
3535 code = run_postgresql_sql_capture_runtime(&admin, query, 1, &value);
3536 if (code == 0) {
3537 code = emit_digit_bool_runtime(value != NULL && streq(value, "1"));
3538 }
3539cleanup:
3540 free(escaped_database);
3541 free(value);
3544 return code;
3545}
3546
3547/**
3548 * @brief Creates one PostgreSQL database when it is missing.
3549 * @param argc Command argument count.
3550 * @param argv Command argument vector.
3551 * @return Native status code.
3552 */
3554 const char *database_name = json_option_value_runtime(argc, argv, "--database");
3555 DynamicPostgresqlRuntimeConfig config = {0};
3557 char *escaped_database = NULL;
3558 char exists_query[2048];
3559 char create_query[2048];
3560 char *value = NULL;
3561 int code = 64;
3562 if (database_name == NULL || database_name[0] == '\0' || !identifier_safe_runtime(database_name)) {
3563 return 64;
3564 }
3565 if (!load_cli_postgresql_runtime_config(argc, argv, &config) || !duplicate_postgresql_runtime_config_with_database_runtime(&config, "postgres", &admin)) {
3566 code = 69;
3567 goto cleanup;
3568 }
3569 escaped_database = sql_literal_escape_dup_runtime(database_name);
3570 if (escaped_database == NULL) {
3571 code = 69;
3572 goto cleanup;
3573 }
3574 snprintf(exists_query, sizeof(exists_query), "SELECT 1 FROM pg_database WHERE datname = '%s' LIMIT 1;", escaped_database);
3575 code = run_postgresql_sql_capture_runtime(&admin, exists_query, 1, &value);
3576 if (code != 0) {
3577 goto cleanup;
3578 }
3579 if (value != NULL && streq(value, "1")) {
3580 code = 0;
3581 goto cleanup;
3582 }
3583 snprintf(create_query, sizeof(create_query), "CREATE DATABASE \"%s\";", database_name);
3584 code = run_postgresql_sql_capture_runtime(&admin, create_query, 0, NULL);
3585cleanup:
3586 free(escaped_database);
3587 free(value);
3590 return code;
3591}
3592
3593/**
3594 * @brief Emits whether one PostgreSQL table exists in the public schema.
3595 * @param argc Command argument count.
3596 * @param argv Command argument vector.
3597 * @return Native status code.
3598 */
3599static int handle_db_postgresql_table_exists_command(int argc, char **argv) {
3600 const char *table_name = json_option_value_runtime(argc, argv, "--table");
3601 DynamicPostgresqlRuntimeConfig config = {0};
3602 char *escaped_table = NULL;
3603 char query[2048];
3604 char *value = NULL;
3605 int code = 64;
3606 if (!identifier_safe_runtime(table_name) || !load_cli_postgresql_runtime_config(argc, argv, &config)) {
3607 return 64;
3608 }
3609 escaped_table = sql_literal_escape_dup_runtime(table_name);
3610 if (escaped_table == NULL) {
3611 code = 69;
3612 goto cleanup;
3613 }
3614 snprintf(query, sizeof(query), "SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '%s' LIMIT 1;", escaped_table);
3615 code = run_postgresql_sql_capture_runtime(&config, query, 1, &value);
3616 if (code == 0) {
3617 code = emit_digit_bool_runtime(value != NULL && streq(value, "1"));
3618 }
3619cleanup:
3620 free(escaped_table);
3621 free(value);
3623 return code;
3624}
3625
3626/**
3627 * @brief Emits one PostgreSQL public table row count.
3628 * @param argc Command argument count.
3629 * @param argv Command argument vector.
3630 * @return Native status code.
3631 */
3632static int handle_db_postgresql_table_row_count_command(int argc, char **argv) {
3633 const char *table_name = json_option_value_runtime(argc, argv, "--table");
3634 DynamicPostgresqlRuntimeConfig config = {0};
3635 char query[1024];
3636 char *value = NULL;
3637 int code = 64;
3638 if (!identifier_safe_runtime(table_name) || !load_cli_postgresql_runtime_config(argc, argv, &config)) {
3639 return 64;
3640 }
3641 snprintf(query, sizeof(query), "SELECT COUNT(*) FROM \"%s\";", table_name);
3642 code = run_postgresql_sql_capture_runtime(&config, query, 1, &value);
3643 if (code == 0) {
3644 fputs(value != NULL ? value : "0", stdout);
3645 }
3646 free(value);
3648 return code;
3649}
3650
3651/**
3652 * @brief Emits the current PostgreSQL public table count.
3653 * @param argc Command argument count.
3654 * @param argv Command argument vector.
3655 * @return Native status code.
3656 */
3657static int handle_db_postgresql_public_table_count_command(int argc, char **argv) {
3658 DynamicPostgresqlRuntimeConfig config = {0};
3659 char *value = NULL;
3660 int code = 64;
3661 if (!load_cli_postgresql_runtime_config(argc, argv, &config)) {
3662 return 64;
3663 }
3664 code = run_postgresql_sql_capture_runtime(&config, "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';", 1, &value);
3665 if (code == 0) {
3666 fputs(value != NULL ? value : "0", stdout);
3667 }
3668 free(value);
3670 return code;
3671}
3672
3673/**
3674 * @brief Emits one PostgreSQL migration applied count.
3675 * @param argc Command argument count.
3676 * @param argv Command argument vector.
3677 * @return Native status code.
3678 */
3680 const char *migration_id = json_option_value_runtime(argc, argv, "--migration-id");
3681 const char *backend = json_option_value_runtime(argc, argv, "--backend");
3682 DynamicPostgresqlRuntimeConfig config = {0};
3683 char *escaped_id = NULL;
3684 char *escaped_backend = NULL;
3685 char query[2048];
3686 char *value = NULL;
3687 int code = 64;
3688 if (migration_id == NULL || backend == NULL || !load_cli_postgresql_runtime_config(argc, argv, &config)) {
3689 return 64;
3690 }
3691 escaped_id = sql_literal_escape_dup_runtime(migration_id);
3692 escaped_backend = sql_literal_escape_dup_runtime(backend);
3693 if (escaped_id == NULL || escaped_backend == NULL) {
3694 code = 69;
3695 goto cleanup;
3696 }
3697 snprintf(query, sizeof(query), "SELECT COUNT(*) FROM pharos_schema_migration WHERE migration_id = '%s' AND backend = '%s';", escaped_id, escaped_backend);
3698 code = run_postgresql_sql_capture_runtime(&config, query, 1, &value);
3699 if (code == 0) {
3700 fputs(value != NULL ? value : "0", stdout);
3701 }
3702cleanup:
3703 free(escaped_id);
3704 free(escaped_backend);
3705 free(value);
3707 return code;
3708}
3709
3710/**
3711 * @brief Emits one PostgreSQL migration applied schema version.
3712 * @param argc Command argument count.
3713 * @param argv Command argument vector.
3714 * @return Native status code.
3715 */
3717 const char *migration_id = json_option_value_runtime(argc, argv, "--migration-id");
3718 const char *backend = json_option_value_runtime(argc, argv, "--backend");
3719 DynamicPostgresqlRuntimeConfig config = {0};
3720 char *escaped_id = NULL;
3721 char *escaped_backend = NULL;
3722 char query[2048];
3723 char *value = NULL;
3724 int code = 64;
3725 if (migration_id == NULL || backend == NULL || !load_cli_postgresql_runtime_config(argc, argv, &config)) {
3726 return 64;
3727 }
3728 escaped_id = sql_literal_escape_dup_runtime(migration_id);
3729 escaped_backend = sql_literal_escape_dup_runtime(backend);
3730 if (escaped_id == NULL || escaped_backend == NULL) {
3731 code = 69;
3732 goto cleanup;
3733 }
3734 snprintf(query, sizeof(query), "SELECT schema_version FROM pharos_schema_migration WHERE migration_id = '%s' AND backend = '%s' ORDER BY applied_at DESC LIMIT 1;", escaped_id, escaped_backend);
3735 code = run_postgresql_sql_capture_runtime(&config, query, 1, &value);
3736 if (code == 0) {
3737 fputs(value != NULL ? value : "", stdout);
3738 }
3739cleanup:
3740 free(escaped_id);
3741 free(escaped_backend);
3742 free(value);
3744 return code;
3745}
3746
3747/**
3748 * @brief Emits one PostgreSQL migration applied checksum.
3749 * @param argc Command argument count.
3750 * @param argv Command argument vector.
3751 * @return Native status code.
3752 */
3754 const char *migration_id = json_option_value_runtime(argc, argv, "--migration-id");
3755 const char *backend = json_option_value_runtime(argc, argv, "--backend");
3756 DynamicPostgresqlRuntimeConfig config = {0};
3757 char *escaped_id = NULL;
3758 char *escaped_backend = NULL;
3759 char query[2048];
3760 char *value = NULL;
3761 int code = 64;
3762 if (migration_id == NULL || backend == NULL || !load_cli_postgresql_runtime_config(argc, argv, &config)) {
3763 return 64;
3764 }
3765 escaped_id = sql_literal_escape_dup_runtime(migration_id);
3766 escaped_backend = sql_literal_escape_dup_runtime(backend);
3767 if (escaped_id == NULL || escaped_backend == NULL) {
3768 code = 69;
3769 goto cleanup;
3770 }
3771 snprintf(query, sizeof(query), "SELECT checksum_sha256 FROM pharos_schema_migration WHERE migration_id = '%s' AND backend = '%s' ORDER BY applied_at DESC LIMIT 1;", escaped_id, escaped_backend);
3772 code = run_postgresql_sql_capture_runtime(&config, query, 1, &value);
3773 if (code == 0) {
3774 fputs(value != NULL ? value : "", stdout);
3775 }
3776cleanup:
3777 free(escaped_id);
3778 free(escaped_backend);
3779 free(value);
3781 return code;
3782}
3783
3784/**
3785 * @brief Updates one PostgreSQL migration checksum when the ledger row is missing one.
3786 * @param argc Command argument count.
3787 * @param argv Command argument vector.
3788 * @return Native status code.
3789 */
3791 const char *migration_id = json_option_value_runtime(argc, argv, "--migration-id");
3792 const char *backend = json_option_value_runtime(argc, argv, "--backend");
3793 const char *checksum = json_option_value_runtime(argc, argv, "--checksum");
3794 DynamicPostgresqlRuntimeConfig config = {0};
3795 char *escaped_id = NULL;
3796 char *escaped_backend = NULL;
3797 char *escaped_checksum = NULL;
3798 char query[4096];
3799 int code = 64;
3800 if (migration_id == NULL || backend == NULL || checksum == NULL || !load_cli_postgresql_runtime_config(argc, argv, &config)) {
3801 return 64;
3802 }
3803 escaped_id = sql_literal_escape_dup_runtime(migration_id);
3804 escaped_backend = sql_literal_escape_dup_runtime(backend);
3805 escaped_checksum = sql_literal_escape_dup_runtime(checksum);
3806 if (escaped_id == NULL || escaped_backend == NULL || escaped_checksum == NULL) {
3807 code = 69;
3808 goto cleanup;
3809 }
3810 snprintf(query, sizeof(query), "UPDATE pharos_schema_migration SET checksum_sha256 = '%s' WHERE migration_id = '%s' AND backend = '%s' AND (checksum_sha256 IS NULL OR BTRIM(checksum_sha256) = '');", escaped_checksum, escaped_id, escaped_backend);
3811 code = run_postgresql_sql_capture_runtime(&config, query, 0, NULL);
3812cleanup:
3813 free(escaped_id);
3814 free(escaped_backend);
3815 free(escaped_checksum);
3817 return code;
3818}
3819
3820/**
3821 * @brief Emits the current PostgreSQL audit event count.
3822 * @param argc Command argument count.
3823 * @param argv Command argument vector.
3824 * @return Native status code.
3825 */
3826static int handle_db_postgresql_audit_event_count_command(int argc, char **argv) {
3827 DynamicPostgresqlRuntimeConfig config = {0};
3828 char *table_exists = NULL;
3829 char *count_text = NULL;
3830 int code = 64;
3831 if (!load_cli_postgresql_runtime_config(argc, argv, &config)) {
3832 return 64;
3833 }
3834 code = run_postgresql_sql_capture_runtime(&config, "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'audit_event');", 1, &table_exists);
3835 if (code == 0) {
3836 if (table_exists != NULL && streq(table_exists, "t")) {
3837 code = run_postgresql_sql_capture_runtime(&config, "SELECT COUNT(*) FROM audit_event;", 1, &count_text);
3838 if (code == 0) {
3839 fputs(count_text != NULL ? count_text : "0", stdout);
3840 }
3841 } else {
3842 fputs("0", stdout);
3843 code = 0;
3844 }
3845 }
3846 free(table_exists);
3847 free(count_text);
3849 return code;
3850}
3851
3852/**
3853 * @brief Emits the first-line output of one PostgreSQL query.
3854 * @param argc Command argument count.
3855 * @param argv Command argument vector.
3856 * @return Native status code.
3857 */
3858static int handle_db_postgresql_query_value_command(int argc, char **argv) {
3859 const char *sql = json_option_value_runtime(argc, argv, "--sql");
3860 DynamicPostgresqlRuntimeConfig config = {0};
3861 char *value = NULL;
3862 int code = 64;
3863 if (sql == NULL || !load_cli_postgresql_runtime_config(argc, argv, &config)) {
3864 return 64;
3865 }
3866 code = run_postgresql_sql_capture_runtime(&config, sql, 1, &value);
3867 if (code == 0 && value != NULL) {
3868 fputs(value, stdout);
3869 }
3870 free(value);
3872 return code;
3873}
3874
3875/**
3876 * @brief Executes one PostgreSQL SQL text payload.
3877 * @param argc Command argument count.
3878 * @param argv Command argument vector.
3879 * @return Native status code.
3880 */
3881static int handle_db_postgresql_exec_command(int argc, char **argv) {
3882 const char *sql = json_option_value_runtime(argc, argv, "--sql");
3883 DynamicPostgresqlRuntimeConfig config = {0};
3884 int code = 64;
3885 if (sql == NULL || !load_cli_postgresql_runtime_config(argc, argv, &config)) {
3886 return 64;
3887 }
3888 code = run_postgresql_sql_capture_runtime(&config, sql, 0, NULL);
3890 return code;
3891}
3892
3893/**
3894 * @brief Executes one PostgreSQL SQL file.
3895 * @param argc Command argument count.
3896 * @param argv Command argument vector.
3897 * @return Native status code.
3898 */
3899static int handle_db_postgresql_exec_file_command(int argc, char **argv) {
3900 const char *file_path = json_option_value_runtime(argc, argv, "--file");
3901 DynamicPostgresqlRuntimeConfig config = {0};
3902 char *sql = NULL;
3903 int code = 64;
3904 if (file_path == NULL || file_path[0] == '\0' || !load_cli_postgresql_runtime_config(argc, argv, &config)) {
3905 return 64;
3906 }
3907 sql = read_file_text(file_path);
3908 if (sql == NULL) {
3910 return 66;
3911 }
3912 code = run_postgresql_sql_capture_runtime(&config, sql, 0, NULL);
3913 free(sql);
3915 return code;
3916}
3917
3918/**
3919 * @brief Emits a stable file signature for change detection.
3920 * @param argc Command argument count.
3921 * @param argv Command argument vector.
3922 * @return Native status code.
3923 */
3924static int handle_dynamic_file_signature_command(int argc, char **argv) {
3925 const char *path = json_option_value_runtime(argc, argv, "--path");
3926 unsigned char *bytes = NULL;
3927 size_t length = 0;
3928 size_t index = 0;
3929 unsigned long long hash = 1469598103934665603ULL;
3930 if (path == NULL || path[0] == '\0') {
3931 return 64;
3932 }
3933 if (!path_exists(path)) {
3934 fputs("missing", stdout);
3935 return 0;
3936 }
3937 bytes = read_file_bytes_dup_runtime(path, &length);
3938 if (bytes == NULL) {
3939 return 66;
3940 }
3941 for (index = 0; index < length; index++) {
3942 hash ^= (unsigned long long)bytes[index];
3943 hash *= 1099511628211ULL;
3944 }
3945 fprintf(stdout, "%016llx:%llu", hash, (unsigned long long)length);
3946 free(bytes);
3947 return 0;
3948}
3949
3950/**
3951 * @brief Emits canonical exported JSON for a sqlite-backed dynamic relational runtime.
3952 * @param argc Command argument count.
3953 * @param argv Command argument vector.
3954 * @return Native status code.
3955 */
3957 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
3958 const char *runtime_conf_path = json_option_value_runtime(argc, argv, "--runtime-conf");
3959 const char *app_id = json_option_value_runtime(argc, argv, "--app-id");
3960 char *export_json = NULL;
3961 if (app_root == NULL || app_root[0] == '\0') {
3962 return 64;
3963 }
3964 export_json = native_dynamic_relational_runtime_export_json_dup(app_root, runtime_conf_path, app_id);
3965 if (export_json == NULL) {
3966 return 69;
3967 }
3968 fputs(export_json, stdout);
3969 free(export_json);
3970 return 0;
3971}
3972
3973/**
3974 * @brief Emits the current audit-event count for a sqlite-backed dynamic relational runtime.
3975 * @param argc Command argument count.
3976 * @param argv Command argument vector.
3977 * @return Native status code.
3978 */
3980 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
3981 const char *runtime_conf_path = json_option_value_runtime(argc, argv, "--runtime-conf");
3982 const char *app_id = json_option_value_runtime(argc, argv, "--app-id");
3983 char *count_text = NULL;
3984 if (app_root == NULL || app_root[0] == '\0') {
3985 return 64;
3986 }
3987 count_text = native_dynamic_relational_runtime_audit_count_dup(app_root, runtime_conf_path, app_id);
3988 if (count_text == NULL) {
3989 return 69;
3990 }
3991 fputs(count_text, stdout);
3992 free(count_text);
3993 return 0;
3994}
3995
3996/**
3997 * @brief Imports canonical state into a sqlite-backed dynamic relational runtime.
3998 * @param argc Command argument count.
3999 * @param argv Command argument vector.
4000 * @return Native status code.
4001 */
4003 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
4004 const char *runtime_conf_path = json_option_value_runtime(argc, argv, "--runtime-conf");
4005 const char *app_id = json_option_value_runtime(argc, argv, "--app-id");
4006 const char *state_path = json_option_value_runtime(argc, argv, "--state");
4007 const char *expected_audit_count = json_option_value_runtime(argc, argv, "--expected-audit-count");
4008 return native_dynamic_relational_runtime_import_state(app_root, runtime_conf_path, app_id, state_path, expected_audit_count);
4009}
4010
4011/**
4012 * @brief Emits whether the sqlite dynamic runtime prerequisites are currently satisfied.
4013 * @param argc Command argument count.
4014 * @param argv Command argument vector.
4015 * @return Native status code.
4016 */
4017static int handle_dynamic_sqlite_runtime_ready_command(int argc, char **argv) {
4018 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
4019 const char *runtime_conf_path = json_option_value_runtime(argc, argv, "--runtime-conf");
4020 const char *app_id = json_option_value_runtime(argc, argv, "--app-id");
4021 if (app_root == NULL || app_root[0] == '\0') {
4022 return 64;
4023 }
4024 fprintf(stdout, "%d", native_dynamic_relational_sqlite_runtime_ready(app_root, runtime_conf_path, app_id) ? 1 : 0);
4025 return 0;
4026}
4027
4028/**
4029 * @brief Prepares the sqlite dynamic runtime schema, seed data, and migration ledger.
4030 * @param argc Command argument count.
4031 * @param argv Command argument vector.
4032 * @return Native status code.
4033 */
4034static int handle_dynamic_postgresql_runtime_ready_command(int argc, char **argv) {
4035 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
4036 const char *runtime_conf_path = json_option_value_runtime(argc, argv, "--runtime-conf");
4037 if (app_root == NULL || app_root[0] == '\0') {
4038 return 64;
4039 }
4040 fprintf(stdout, "%d", native_dynamic_relational_postgresql_runtime_ready(app_root, runtime_conf_path) ? 1 : 0);
4041 return 0;
4042}
4043
4044/**
4045 * @brief Prepares the PostgreSQL dynamic runtime schema, seed data, and migration ledger.
4046 * @param argc Command argument count.
4047 * @param argv Command argument vector.
4048 * @return Native status code.
4049 */
4050static int handle_dynamic_postgresql_runtime_prepare_command(int argc, char **argv) {
4051 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
4052 const char *runtime_conf_path = json_option_value_runtime(argc, argv, "--runtime-conf");
4053 if (app_root == NULL || app_root[0] == '\0') {
4054 return 64;
4055 }
4056 return native_dynamic_relational_postgresql_runtime_prepare(app_root, runtime_conf_path);
4057}
4058
4059static int handle_dynamic_sqlite_runtime_prepare_command(int argc, char **argv) {
4060 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
4061 const char *runtime_conf_path = json_option_value_runtime(argc, argv, "--runtime-conf");
4062 const char *app_id = json_option_value_runtime(argc, argv, "--app-id");
4063 if (app_root == NULL || app_root[0] == '\0') {
4064 return 64;
4065 }
4066 return native_dynamic_relational_sqlite_runtime_prepare(app_root, runtime_conf_path, app_id);
4067}
4068
4069static yyjson_val *migration_backend_record_runtime(yyjson_val *root, const char *backend_value) {
4070 yyjson_val *backends = native_json_obj_get_array(root, "backends");
4071 yyjson_arr_iter iter;
4072 yyjson_val *item = NULL;
4073 if (backends == NULL || backend_value == NULL || !yyjson_arr_iter_init(backends, &iter)) {
4074 return NULL;
4075 }
4076 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
4077 if (yyjson_equals_str(native_json_obj_get(item, "backend"), backend_value)) {
4078 return item;
4079 }
4080 }
4081 return NULL;
4082}
4083
4084static yyjson_val *migration_record_by_id_runtime(yyjson_val *root, const char *migration_id) {
4085 yyjson_val *migrations = native_json_obj_get_array(root, "migrations");
4086 yyjson_arr_iter iter;
4087 yyjson_val *item = NULL;
4088 if (migrations == NULL || migration_id == NULL || !yyjson_arr_iter_init(migrations, &iter)) {
4089 return NULL;
4090 }
4091 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
4092 if (yyjson_equals_str(native_json_obj_get(item, "migration_id"), migration_id)) {
4093 return item;
4094 }
4095 }
4096 return NULL;
4097}
4098
4099static int handle_file_migration_backend_list_lines_command(int argc, char **argv) {
4100 const char *file_path = json_option_value_runtime(argc, argv, "--file");
4101 const char *backend_value = json_option_value_runtime(argc, argv, "--backend");
4102 const char *list_key = json_option_value_runtime(argc, argv, "--list-key");
4103 yyjson_doc *doc = NULL;
4104 yyjson_val *root = NULL;
4105 yyjson_val *backend = NULL;
4106 yyjson_val *list = NULL;
4107 yyjson_arr_iter iter;
4108 yyjson_val *item = NULL;
4109 if (file_path == NULL || backend_value == NULL || list_key == NULL || list_key[0] == '\0') {
4110 return 64;
4111 }
4112 doc = native_json_doc_load_file(file_path);
4113 if (doc == NULL) {
4114 return 66;
4115 }
4116 root = yyjson_doc_get_root(doc);
4117 backend = migration_backend_record_runtime(root, backend_value);
4118 list = backend != NULL ? native_json_obj_get_array(backend, list_key) : NULL;
4119 if (list != NULL && yyjson_arr_iter_init(list, &iter)) {
4120 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
4121 if (yyjson_is_str(item)) {
4122 fwrite(yyjson_get_str(item), 1, yyjson_get_len(item), stdout);
4123 fputc('\n', stdout);
4124 }
4125 }
4126 }
4128 return 0;
4129}
4130
4131static int handle_file_migration_backend_record_json_command(int argc, char **argv) {
4132 const char *file_path = json_option_value_runtime(argc, argv, "--file");
4133 const char *backend_value = json_option_value_runtime(argc, argv, "--backend");
4134 yyjson_doc *doc = NULL;
4135 yyjson_val *root = NULL;
4136 yyjson_val *backend = NULL;
4137 if (file_path == NULL || backend_value == NULL || backend_value[0] == '\0') {
4138 return 64;
4139 }
4140 doc = native_json_doc_load_file(file_path);
4141 if (doc == NULL) {
4142 return 66;
4143 }
4144 root = yyjson_doc_get_root(doc);
4145 backend = migration_backend_record_runtime(root, backend_value);
4146 if (backend != NULL) {
4147 int code = emit_compact_json_value_runtime(backend);
4149 return code;
4150 }
4152 return 0;
4153}
4154
4155static int handle_file_migration_rows_lines_command(int argc, char **argv) {
4156 const char *file_path = json_option_value_runtime(argc, argv, "--file");
4157 const char *backend_value = json_option_value_runtime(argc, argv, "--backend");
4158 yyjson_doc *doc = NULL;
4159 yyjson_val *root = NULL;
4160 yyjson_val *migrations = NULL;
4161 yyjson_arr_iter iter;
4162 yyjson_val *item = NULL;
4163 if (file_path == NULL || backend_value == NULL || backend_value[0] == '\0') {
4164 return 64;
4165 }
4166 doc = native_json_doc_load_file(file_path);
4167 if (doc == NULL) {
4168 return 66;
4169 }
4170 root = yyjson_doc_get_root(doc);
4171 migrations = native_json_obj_get_array(root, "migrations");
4172 if (migrations != NULL && yyjson_arr_iter_init(migrations, &iter)) {
4173 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
4174 yyjson_val *backend_files = native_json_obj_get(item, "backend_files");
4175 yyjson_val *backend_obj = backend_files != NULL && yyjson_is_obj(backend_files) ? native_json_obj_get(backend_files, backend_value) : NULL;
4176 yyjson_val *forward_files = backend_obj != NULL ? native_json_obj_get_array(backend_obj, "forward_sql_files") : NULL;
4177 if (forward_files != NULL && yyjson_arr_size(forward_files) > 0) {
4178 char *migration_id = native_json_obj_get_string_dup(item, "migration_id");
4179 long schema_version = native_json_obj_get_long_default(item, "schema_version", 0, NULL);
4180 if (migration_id != NULL) {
4181 fprintf(stdout, "%s\t%ld\n", migration_id, schema_version);
4182 }
4183 free(migration_id);
4184 }
4185 }
4186 }
4188 return 0;
4189}
4190
4191static int handle_file_migration_backend_files_json_command(int argc, char **argv) {
4192 const char *file_path = json_option_value_runtime(argc, argv, "--file");
4193 const char *backend_value = json_option_value_runtime(argc, argv, "--backend");
4194 const char *migration_id = json_option_value_runtime(argc, argv, "--migration-id");
4195 const char *field_name = json_option_value_runtime(argc, argv, "--field-name");
4196 yyjson_doc *doc = NULL;
4197 yyjson_val *root = NULL;
4198 yyjson_val *migration = NULL;
4199 yyjson_val *backend_files = NULL;
4200 yyjson_val *backend_obj = NULL;
4201 yyjson_val *field_value = NULL;
4202 yyjson_val *backend_record = NULL;
4203 yyjson_val *migrations = NULL;
4204 if (file_path == NULL || backend_value == NULL || migration_id == NULL || field_name == NULL || field_name[0] == '\0') {
4205 return 64;
4206 }
4207 doc = native_json_doc_load_file(file_path);
4208 if (doc == NULL) {
4209 return 66;
4210 }
4211 root = yyjson_doc_get_root(doc);
4212 migration = migration_record_by_id_runtime(root, migration_id);
4213 if (migration != NULL) {
4214 backend_files = native_json_obj_get(migration, "backend_files");
4215 backend_obj = backend_files != NULL && yyjson_is_obj(backend_files) ? native_json_obj_get(backend_files, backend_value) : NULL;
4216 field_value = backend_obj != NULL ? native_json_obj_get_array(backend_obj, field_name) : NULL;
4217 if (field_value != NULL) {
4218 int code = emit_compact_json_value_runtime(field_value);
4220 return code;
4221 }
4222 }
4223 migrations = native_json_obj_get_array(root, "migrations");
4224 if (migrations != NULL && yyjson_arr_size(migrations) == 1) {
4225 backend_record = migration_backend_record_runtime(root, backend_value);
4226 if (backend_record != NULL) {
4227 if (streq(field_name, "forward_sql_files")) {
4228 field_value = native_json_obj_get_array(backend_record, "schema_files");
4229 } else if (streq(field_name, "seed_sql_files")) {
4230 field_value = native_json_obj_get_array(backend_record, "seed_files");
4231 }
4232 }
4233 if (field_value != NULL) {
4234 int code = emit_compact_json_value_runtime(field_value);
4236 return code;
4237 }
4238 }
4239 fputs("[]", stdout);
4241 return 0;
4242}
4243
4244static int handle_file_normalize_ucal_state_command(int argc, char **argv) {
4245 const char *file_path = json_option_value_runtime(argc, argv, "--file");
4246 static const char *sections[] = {
4247 "users", "sessions", "tenants", "tenant_memberships", "password_resets", "invites", "rate_limit_events",
4248 "businesses", "services", "bookings", "appointments", "subscriptions", "ratings", "google_calendar_connections",
4249 "technicians", "business_availability", "technician_availability", "email_verifications", "idempotency_requests",
4250 "workflow_executions", "workflow_execution_events", "job_executions", "job_execution_attempts", "audit_log", "webhook_receipts"
4251 };
4252 yyjson_doc *doc = NULL;
4253 yyjson_val *root = NULL;
4254 yyjson_mut_doc *mut_doc = NULL;
4255 yyjson_mut_val *mut_root = NULL;
4256 size_t i = 0;
4257 int code = 69;
4258 if (file_path == NULL || file_path[0] == '\0') {
4259 return 64;
4260 }
4261 doc = native_json_doc_load_file(file_path);
4262 if (doc == NULL) {
4263 return 66;
4264 }
4265 root = yyjson_doc_get_root(doc);
4266 mut_doc = yyjson_doc_mut_copy(doc, NULL);
4267 mut_root = mut_doc != NULL ? yyjson_mut_doc_get_root(mut_doc) : NULL;
4268 if (root == NULL || !yyjson_is_obj(root) || mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
4270 yyjson_mut_doc_free(mut_doc);
4271 return 69;
4272 }
4273 if (yyjson_mut_obj_get(mut_root, "schema") == NULL) {
4274 yyjson_mut_obj_add_strcpy(mut_doc, mut_root, "schema", "pharos.ucal_native_core_state.v1");
4275 }
4276 for (i = 0; i < sizeof(sections) / sizeof(sections[0]); i++) {
4277 if (yyjson_mut_obj_get(mut_root, sections[i]) == NULL) {
4278 yyjson_mut_obj_add_val(mut_doc, mut_root, sections[i], yyjson_mut_arr(mut_doc));
4279 }
4280 }
4281 code = emit_compact_json_value_runtime((yyjson_val *)mut_root);
4283 yyjson_mut_doc_free(mut_doc);
4284 return code;
4285}
4286
4287static yyjson_mut_val *ensure_mut_array_field_runtime(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *field_name);
4288
4289static int emit_json_string_runtime(const char *text) {
4290 yyjson_mut_doc *doc = NULL;
4291 yyjson_mut_val *value = NULL;
4292 char *json = NULL;
4293 if (text == NULL) {
4294 fputs("null", stdout);
4295 return 0;
4296 }
4297 doc = yyjson_mut_doc_new(NULL);
4298 if (doc == NULL) {
4299 return 69;
4300 }
4301 value = yyjson_mut_strcpy(doc, text);
4302 json = value != NULL ? yyjson_mut_val_write(value, YYJSON_WRITE_NOFLAG, NULL) : NULL;
4303 if (json == NULL) {
4305 return 69;
4306 }
4307 fputs(json, stdout);
4308 free(json);
4310 return 0;
4311}
4312
4313static int handle_request_checkbox_id_array_json_command(int argc, char **argv) {
4314 const char *text = json_option_value_runtime(argc, argv, "--text");
4315 const char *prefix = json_option_value_runtime(argc, argv, "--prefix");
4316 char *copy = NULL;
4317 char *line = NULL;
4318 char *saveptr = NULL;
4319 long values[512];
4320 size_t count = 0;
4321 size_t idx = 0;
4322 if (text == NULL || prefix == NULL || prefix[0] == '\0') {
4323 return 64;
4324 }
4325 copy = strdup(text);
4326 if (copy == NULL) {
4327 return 69;
4328 }
4329 line = strtok_r(copy, "&", &saveptr);
4330 while (line != NULL) {
4331 char *eq = strchr(line, '=');
4332 if (eq != NULL) {
4333 size_t key_len = (size_t)(eq - line);
4334 const char *value = eq + 1;
4335 if (strncmp(line, prefix, strlen(prefix)) == 0) {
4336 size_t i = strlen(prefix);
4337 int key_ok = key_len > i;
4338 for (; i < key_len; i++) {
4339 if (!isdigit((unsigned char)line[i])) {
4340 key_ok = 0;
4341 break;
4342 }
4343 }
4344 if (key_ok && value[0] != '\0') {
4345 char *endptr = NULL;
4346 long parsed = strtol(value, &endptr, 10);
4347 if (endptr != value && *endptr == '\0') {
4348 int seen = 0;
4349 size_t j = 0;
4350 for (j = 0; j < count; j++) {
4351 if (values[j] == parsed) {
4352 seen = 1;
4353 break;
4354 }
4355 }
4356 if (!seen && count < (sizeof(values) / sizeof(values[0]))) {
4357 values[count++] = parsed;
4358 }
4359 }
4360 }
4361 }
4362 }
4363 line = strtok_r(NULL, "&", &saveptr);
4364 }
4365 free(copy);
4366 fputc('[', stdout);
4367 for (idx = 0; idx < count; idx++) {
4368 if (idx > 0) {
4369 fputc(',', stdout);
4370 }
4371 fprintf(stdout, "%ld", values[idx]);
4372 }
4373 fputc(']', stdout);
4374 return 0;
4375}
4376
4377static int trim_ascii_whitespace_range_runtime(const char *start, size_t len, const char **trimmed_start, size_t *trimmed_len) {
4378 size_t begin = 0;
4379 size_t end = len;
4380 if (start == NULL || trimmed_start == NULL || trimmed_len == NULL) {
4381 return 0;
4382 }
4383 while (begin < len && isspace((unsigned char)start[begin])) {
4384 begin++;
4385 }
4386 while (end > begin && isspace((unsigned char)start[end - 1])) {
4387 end--;
4388 }
4389 *trimmed_start = start + begin;
4390 *trimmed_len = end - begin;
4391 return 1;
4392}
4393
4394static int digits_only_runtime(const char *text, size_t len) {
4395 size_t i = 0;
4396 if (text == NULL || len == 0) {
4397 return 0;
4398 }
4399 for (i = 0; i < len; i++) {
4400 if (!isdigit((unsigned char)text[i])) {
4401 return 0;
4402 }
4403 }
4404 return 1;
4405}
4406
4408 const char *text = json_option_value_runtime(argc, argv, "--text");
4409 char *copy = NULL;
4410 char *line = NULL;
4411 char *saveptr = NULL;
4412 yyjson_mut_doc *mut_doc = NULL;
4413 yyjson_mut_val *array = NULL;
4414 int code = 69;
4415 if (text == NULL) {
4416 return 64;
4417 }
4418 copy = strdup(text);
4419 mut_doc = yyjson_mut_doc_new(NULL);
4420 array = yyjson_mut_arr(mut_doc);
4421 if (copy == NULL || mut_doc == NULL || array == NULL) {
4422 free(copy);
4423 yyjson_mut_doc_free(mut_doc);
4424 return 69;
4425 }
4426 yyjson_mut_doc_set_root(mut_doc, array);
4427 line = strtok_r(copy, "\n", &saveptr);
4428 while (line != NULL) {
4429 size_t len = strlen(line);
4430 while (len > 0 && line[len - 1] == '\r') {
4431 line[--len] = '\0';
4432 }
4433 if (len > 0) {
4434 char *first = strchr(line, '|');
4435 char *second = first != NULL ? strchr(first + 1, '|') : NULL;
4436 if (first != NULL && second != NULL) {
4437 const char *label_start = NULL;
4438 const char *duration_start = NULL;
4439 const char *cost_start = NULL;
4440 size_t label_len = 0;
4441 size_t duration_len = 0;
4442 size_t cost_len = 0;
4443 trim_ascii_whitespace_range_runtime(line, (size_t)(first - line), &label_start, &label_len);
4444 trim_ascii_whitespace_range_runtime(first + 1, (size_t)(second - (first + 1)), &duration_start, &duration_len);
4445 trim_ascii_whitespace_range_runtime(second + 1, strlen(second + 1), &cost_start, &cost_len);
4446 if (label_len > 0 && duration_len > 0 && cost_len > 0 && digits_only_runtime(duration_start, duration_len)) {
4447 char *label = dup_range(label_start, label_len);
4448 char *duration_text = dup_range(duration_start, duration_len);
4449 char *cost_value = dup_range(cost_start, cost_len);
4450 yyjson_mut_val *obj = yyjson_mut_obj(mut_doc);
4451 long duration_value = duration_text != NULL ? strtol(duration_text, NULL, 10) : 0;
4452 if (label != NULL && duration_text != NULL && cost_value != NULL && obj != NULL && yyjson_mut_obj_add_strcpy(mut_doc, obj, "label", label) && yyjson_mut_obj_add_int(mut_doc, obj, "duration_minutes", duration_value) && yyjson_mut_obj_add_strcpy(mut_doc, obj, "cost", cost_value) && yyjson_mut_arr_append(array, obj)) {
4453 }
4454 free(label);
4455 free(duration_text);
4456 free(cost_value);
4457 }
4458 }
4459 }
4460 line = strtok_r(NULL, "\n", &saveptr);
4461 }
4463 free(copy);
4464 yyjson_mut_doc_free(mut_doc);
4465 return code;
4466}
4467
4468static int handle_object_merge_command(int argc, char **argv) {
4469 const char *base_json = json_option_value_runtime(argc, argv, "--base-json");
4470 const char *overlay_json = json_option_value_runtime(argc, argv, "--overlay-json");
4471 yyjson_doc *base_doc = NULL;
4472 yyjson_doc *overlay_doc = NULL;
4473 yyjson_val *base_root = NULL;
4474 yyjson_val *overlay_root = NULL;
4475 yyjson_mut_doc *mut_doc = NULL;
4476 yyjson_mut_val *mut_root = NULL;
4477 yyjson_obj_iter iter;
4478 yyjson_val *key = NULL;
4479 yyjson_val *value = NULL;
4480 int code = 0;
4481 if (base_json == NULL || overlay_json == NULL) {
4482 return 64;
4483 }
4484 base_root = load_root_from_text_runtime(base_json, &base_doc);
4485 overlay_root = load_root_from_text_runtime(overlay_json, &overlay_doc);
4486 if (base_root == NULL || !yyjson_is_obj(base_root) || overlay_root == NULL || !yyjson_is_obj(overlay_root)) {
4487 native_json_doc_free(base_doc);
4488 native_json_doc_free(overlay_doc);
4489 return 64;
4490 }
4491 mut_doc = yyjson_mut_doc_new(NULL);
4492 mut_root = yyjson_val_mut_copy(mut_doc, base_root);
4493 yyjson_mut_doc_set_root(mut_doc, mut_root);
4494 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
4495 native_json_doc_free(base_doc);
4496 native_json_doc_free(overlay_doc);
4497 yyjson_mut_doc_free(mut_doc);
4498 return 69;
4499 }
4500 if (yyjson_obj_iter_init(overlay_root, &iter)) {
4501 while ((key = yyjson_obj_iter_next(&iter)) != NULL) {
4502 value = yyjson_obj_iter_get_val(key);
4503 if (yyjson_is_str(key)) {
4504 const char *key_text = yyjson_get_str(key);
4505 yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, key_text), yyjson_val_mut_copy(mut_doc, value));
4506 }
4507 }
4508 }
4510 native_json_doc_free(base_doc);
4511 native_json_doc_free(overlay_doc);
4512 yyjson_mut_doc_free(mut_doc);
4513 return code;
4514}
4515
4516static int handle_object_build_command(int argc, char **argv) {
4517 yyjson_mut_doc *mut_doc = yyjson_mut_doc_new(NULL);
4518 yyjson_mut_val *obj = mut_doc != NULL ? yyjson_mut_obj(mut_doc) : NULL;
4519 int index = 0;
4520 int code = 0;
4521 if (mut_doc == NULL || obj == NULL) {
4522 yyjson_mut_doc_free(mut_doc);
4523 return 69;
4524 }
4525 yyjson_mut_doc_set_root(mut_doc, obj);
4526 while (index < argc) {
4527 const char *option = argv[index];
4528 const char *key = NULL;
4529 const char *value = NULL;
4530 if (streq(option, "--string")) {
4531 if (index + 2 >= argc) {
4532 yyjson_mut_doc_free(mut_doc);
4533 return 64;
4534 }
4535 key = argv[index + 1];
4536 value = argv[index + 2];
4537 if (!yyjson_mut_obj_add_strcpy(mut_doc, obj, key, value != NULL ? value : "")) {
4538 yyjson_mut_doc_free(mut_doc);
4539 return 69;
4540 }
4541 index += 3;
4542 continue;
4543 }
4544 if (streq(option, "--number")) {
4545 char *endptr = NULL;
4546 long number = 0;
4547 if (index + 2 >= argc) {
4548 yyjson_mut_doc_free(mut_doc);
4549 return 64;
4550 }
4551 key = argv[index + 1];
4552 value = argv[index + 2];
4553 number = strtol(value != NULL ? value : "0", &endptr, 10);
4554 if (value == NULL || endptr == value || *endptr != '\0' || !yyjson_mut_obj_add_int(mut_doc, obj, key, number)) {
4555 yyjson_mut_doc_free(mut_doc);
4556 return 64;
4557 }
4558 index += 3;
4559 continue;
4560 }
4561 if (streq(option, "--bool")) {
4562 int bool_value = 0;
4563 if (index + 2 >= argc) {
4564 yyjson_mut_doc_free(mut_doc);
4565 return 64;
4566 }
4567 key = argv[index + 1];
4568 value = argv[index + 2];
4569 bool_value = value != NULL && (streq(value, "true") || streq(value, "1") || streq(value, "yes"));
4570 if (!yyjson_mut_obj_add_bool(mut_doc, obj, key, bool_value)) {
4571 yyjson_mut_doc_free(mut_doc);
4572 return 69;
4573 }
4574 index += 3;
4575 continue;
4576 }
4577 if (streq(option, "--null")) {
4578 if (index + 1 >= argc) {
4579 yyjson_mut_doc_free(mut_doc);
4580 return 64;
4581 }
4582 key = argv[index + 1];
4583 if (!yyjson_mut_obj_add_null(mut_doc, obj, key)) {
4584 yyjson_mut_doc_free(mut_doc);
4585 return 69;
4586 }
4587 index += 2;
4588 continue;
4589 }
4590 if (streq(option, "--json")) {
4591 yyjson_doc *value_doc = NULL;
4592 yyjson_val *value_root = NULL;
4593 if (index + 2 >= argc) {
4594 yyjson_mut_doc_free(mut_doc);
4595 return 64;
4596 }
4597 key = argv[index + 1];
4598 value = argv[index + 2];
4599 value_root = load_root_from_text_runtime(value, &value_doc);
4600 if (value_root == NULL || !yyjson_mut_obj_put(obj, yyjson_mut_strcpy(mut_doc, key), yyjson_val_mut_copy(mut_doc, value_root))) {
4601 native_json_doc_free(value_doc);
4602 yyjson_mut_doc_free(mut_doc);
4603 return 64;
4604 }
4605 native_json_doc_free(value_doc);
4606 index += 3;
4607 continue;
4608 }
4609 yyjson_mut_doc_free(mut_doc);
4610 return 64;
4611 }
4613 yyjson_mut_doc_free(mut_doc);
4614 return code;
4615}
4616
4618 if (yyjson_is_str(value)) {
4619 return strdup(yyjson_get_str(value));
4620 }
4621 if (yyjson_is_num(value)) {
4623 }
4624 if (yyjson_is_bool(value)) {
4625 return strdup(yyjson_get_bool(value) ? "true" : "false");
4626 }
4627 return NULL;
4628}
4629
4630/**
4631 * @brief Checks whether a reference candidate contains unsafe control bytes.
4632 * @param text Candidate URL or path reference text.
4633 * @return 1 when an unsafe control byte is present, else 0.
4634 */
4636 size_t index = 0;
4637 if (text == NULL) {
4638 return 1;
4639 }
4640 for (index = 0; text[index] != '\0'; index++) {
4641 unsigned char ch = (unsigned char)text[index];
4642 if (ch < 0x20 || ch == 0x7f) {
4643 return 1;
4644 }
4645 }
4646 return 0;
4647}
4648
4649/**
4650 * @brief Tests whether a reference begins with a URI scheme prefix.
4651 * @param text Candidate URL or path reference text.
4652 * @return 1 when the text starts with a URI scheme, else 0.
4653 */
4654static int template_reference_has_scheme_runtime(const char *text) {
4655 size_t index = 0;
4656 if (text == NULL || !isalpha((unsigned char)text[0])) {
4657 return 0;
4658 }
4659 for (index = 1; text[index] != '\0'; index++) {
4660 unsigned char ch = (unsigned char)text[index];
4661 if (ch == ':') {
4662 return 1;
4663 }
4664 if (!(isalnum(ch) || ch == '+' || ch == '-' || ch == '.')) {
4665 return 0;
4666 }
4667 }
4668 return 0;
4669}
4670
4671/**
4672 * @brief Tests whether a reference uses one permitted external URL scheme.
4673 * @param text Candidate URL reference text.
4674 * @return 1 when the scheme is explicitly allowed, else 0.
4675 */
4677 if (text == NULL) {
4678 return 0;
4679 }
4680 return strncmp(text, "https://", 8) == 0
4681 || strncmp(text, "http://", 7) == 0
4682 || strncmp(text, "mailto:", 7) == 0
4683 || strncmp(text, "tel:", 4) == 0;
4684}
4685
4686/**
4687 * @brief Tests whether one candidate path reference is safe for HTML URL attributes.
4688 * @param text Candidate path or path-segment text.
4689 * @return 1 when the value is allowed, else 0.
4690 */
4691static int template_path_reference_allowed_runtime(const char *text) {
4692 if (text == NULL) {
4693 return 0;
4694 }
4695 if (text[0] == '\0') {
4696 return 1;
4697 }
4699 return 0;
4700 }
4702 return 0;
4703 }
4704 return 1;
4705}
4706
4707/**
4708 * @brief Tests whether one candidate URL reference is safe for HTML URL attributes.
4709 * @param text Candidate URL, path, query, or fragment reference text.
4710 * @return 1 when the value is allowed, else 0.
4711 */
4712static int template_url_reference_allowed_runtime(const char *text) {
4713 if (text == NULL) {
4714 return 0;
4715 }
4716 if (text[0] == '\0') {
4717 return 1;
4718 }
4720 return 0;
4721 }
4723 return 1;
4724 }
4726 return 0;
4727 }
4728 return 1;
4729}
4730
4731/**
4732 * @brief Appends one template placeholder value using the requested safety mode.
4733 * @param buffer Output text buffer to append into.
4734 * @param value JSON value resolved for the placeholder.
4735 * @param kind Placeholder rendering mode.
4736 * @return 1 on success, else 0.
4737 */
4739 char *raw = NULL;
4740 int ok = 1;
4741 if (buffer == NULL || value == NULL) {
4742 return 1;
4743 }
4745 if (raw == NULL) {
4746 return 1;
4747 }
4748 switch (kind) {
4750 ok = text_buffer_append_text_runtime(buffer, raw);
4751 break;
4754 break;
4757 break;
4759 default:
4761 break;
4762 }
4763 free(raw);
4764 return ok;
4765}
4766
4767/**
4768 * @brief Parses one placeholder token into its rendering mode and JSON key name.
4769 * @param placeholder_name Raw placeholder text between braces.
4770 * @param param_name Receives the effective JSON key name.
4771 * @return Parsed placeholder rendering mode.
4772 */
4773static TemplatePlaceholderKindRuntime template_placeholder_kind_runtime(const char *placeholder_name, const char **param_name) {
4774 const char *colon = NULL;
4775 if (param_name != NULL) {
4776 *param_name = placeholder_name;
4777 }
4778 if (placeholder_name == NULL) {
4780 }
4781 colon = strchr(placeholder_name, ':');
4782 if (colon == NULL || colon == placeholder_name || colon[1] == '\0') {
4784 }
4785 if (strncmp(placeholder_name, "html:", 5) == 0) {
4786 if (param_name != NULL) {
4787 *param_name = placeholder_name + 5;
4788 }
4790 }
4791 if (strncmp(placeholder_name, "path:", 5) == 0) {
4792 if (param_name != NULL) {
4793 *param_name = placeholder_name + 5;
4794 }
4796 }
4797 if (strncmp(placeholder_name, "url:", 4) == 0) {
4798 if (param_name != NULL) {
4799 *param_name = placeholder_name + 4;
4800 }
4802 }
4804}
4805
4806/**
4807 * @brief Renders one template string using one caller-selected default placeholder mode.
4808 * @param template_text Raw template string with placeholders.
4809 * @param params_root JSON object providing replacement values.
4810 * @param default_kind Default rendering mode for unprefixed placeholders.
4811 * @return Newly allocated rendered text, or NULL on failure.
4812 */
4813static char *render_template_text_with_default_kind_dup_runtime(const char *template_text, yyjson_val *params_root, TemplatePlaceholderKindRuntime default_kind) {
4814 TextBufferRuntime buffer = {0};
4815 size_t i = 0;
4816 if (template_text == NULL || params_root == NULL || !yyjson_is_obj(params_root)) {
4817 return NULL;
4818 }
4819 while (template_text[i] != '\0') {
4820 if (template_text[i] == '{') {
4821 size_t start = i + 1;
4822 size_t end = start;
4823 while (template_text[end] != '\0' && template_text[end] != '}') {
4824 end++;
4825 }
4826 if (template_text[end] == '}') {
4827 char *name = dup_range(template_text + start, end - start);
4828 const char *param_name = NULL;
4830 yyjson_val *value = param_name != NULL ? native_json_obj_get(params_root, param_name) : NULL;
4832 kind = default_kind;
4833 }
4834 if (!text_buffer_append_template_value_runtime(&buffer, value, kind)) {
4835 free(name);
4836 text_buffer_free_runtime(&buffer);
4837 return NULL;
4838 }
4839 free(name);
4840 i = end + 1;
4841 continue;
4842 }
4843 }
4844 if (!text_buffer_append_char_runtime(&buffer, template_text[i])) {
4845 text_buffer_free_runtime(&buffer);
4846 return NULL;
4847 }
4848 i++;
4849 }
4850 return text_buffer_take_runtime(&buffer);
4851}
4852
4853static int emit_template_text_runtime(const char *template_text, yyjson_val *params_root) {
4854 char *rendered = render_template_text_dup_runtime(template_text, params_root);
4855 if (rendered == NULL) {
4856 return 64;
4857 }
4858 fputs(rendered, stdout);
4859 free(rendered);
4860 return 0;
4861}
4862
4863char *render_template_text_dup_runtime(const char *template_text, yyjson_val *params_root) {
4865}
4866
4867char *render_path_template_text_dup_runtime(const char *template_text, yyjson_val *params_root) {
4869}
4870
4871char *join_base_relative_path_dup_runtime(const char *base_path, const char *relative_path) {
4872 char *joined = NULL;
4873 size_t needed = 0;
4874 if (base_path == NULL || base_path[0] == '\0' || relative_path == NULL || relative_path[0] == '\0') {
4875 return NULL;
4876 }
4877 needed = strlen(base_path) + 1 + strlen(relative_path) + 1;
4878 joined = (char *)malloc(needed);
4879 if (joined != NULL) {
4880 snprintf(joined, needed, "%s/%s", base_path, relative_path);
4881 }
4882 return joined;
4883}
4884
4885char *render_file_template_dup_runtime(const char *template_file, yyjson_val *params_root) {
4886 char *template_text = NULL;
4887 char *rendered = NULL;
4888 if (template_file == NULL || template_file[0] == '\0' || params_root == NULL || !yyjson_is_obj(params_root)) {
4889 return NULL;
4890 }
4891 template_text = read_file_text(template_file);
4892 if (template_text == NULL) {
4893 return NULL;
4894 }
4895 rendered = render_template_text_dup_runtime(template_text, params_root);
4896 free(template_text);
4897 return rendered;
4898}
4899
4900static char *business_media_url_dup_runtime(const char *business_slug, const char *media_url) {
4901 char *out = NULL;
4902 const char *base_name = NULL;
4903 if (media_url == NULL || media_url[0] == '\0') {
4904 return strdup("");
4905 }
4906 if (business_slug != NULL && business_slug[0] != '\0') {
4907 char expected_prefix[512];
4908 snprintf(expected_prefix, sizeof(expected_prefix), "/media/businesses/%s/", business_slug);
4909 if (strncmp(media_url, expected_prefix, strlen(expected_prefix)) == 0) {
4910 return strdup(media_url);
4911 }
4912 if (strncmp(media_url, "/media/businesses/", 18) == 0) {
4913 base_name = strrchr(media_url, '/');
4914 if (base_name != NULL) {
4915 size_t needed = strlen("/media/businesses/") + strlen(business_slug) + 1 + strlen(base_name + 1) + 1;
4916 out = (char *)malloc(needed);
4917 if (out != NULL) {
4918 snprintf(out, needed, "/media/businesses/%s/%s", business_slug, base_name + 1);
4919 }
4920 return out;
4921 }
4922 }
4923 }
4924 return strdup(media_url);
4925}
4926
4927static char *service_media_url_dup_runtime(const char *business_slug, const char *media_url) {
4928 char *out = NULL;
4929 const char *base_name = NULL;
4930 if (media_url == NULL || media_url[0] == '\0') {
4931 return strdup("");
4932 }
4933 if (business_slug != NULL && business_slug[0] != '\0') {
4934 char expected_prefix[512];
4935 snprintf(expected_prefix, sizeof(expected_prefix), "/media/businesses/%s/services/", business_slug);
4936 if (strncmp(media_url, expected_prefix, strlen(expected_prefix)) == 0) {
4937 return strdup(media_url);
4938 }
4939 if (strncmp(media_url, "/media/businesses/", 18) == 0 && strstr(media_url, "/services/") != NULL) {
4940 base_name = strrchr(media_url, '/');
4941 if (base_name != NULL) {
4942 size_t needed = strlen("/media/businesses/") + strlen(business_slug) + strlen("/services/") + strlen(base_name + 1) + 1;
4943 out = (char *)malloc(needed);
4944 if (out != NULL) {
4945 snprintf(out, needed, "/media/businesses/%s/services/%s", business_slug, base_name + 1);
4946 }
4947 return out;
4948 }
4949 }
4950 if (strncmp(media_url, "/media/services/", 16) == 0) {
4951 base_name = strrchr(media_url, '/');
4952 if (base_name != NULL) {
4953 size_t needed = strlen("/media/businesses/") + strlen(business_slug) + strlen("/services/") + strlen(base_name + 1) + 1;
4954 out = (char *)malloc(needed);
4955 if (out != NULL) {
4956 snprintf(out, needed, "/media/businesses/%s/services/%s", business_slug, base_name + 1);
4957 }
4958 return out;
4959 }
4960 }
4961 }
4962 return strdup(media_url);
4963}
4964
4965static char *app_root_relative_path_dup_runtime(const char *app_root, const char *relative_path);
4966
4967/**
4968 * @brief Resolves one UCAL declared page id to its app-relative page-spec path.
4969 * @param page_id Declared UCAL page id.
4970 * @return Matching relative path, or NULL when the page id is unknown.
4971 */
4972static const char *ucal_declared_page_relative_path_runtime(const char *page_id) {
4973 static const struct {
4974 const char *page_id;
4975 const char *relative_path;
4976 } entries[] = {
4977 { "account_created", "templates/registration/account_created.page.json" },
4978 { "login", "templates/registration/login.page.json" },
4979 { "register", "templates/registration/register.page.json" },
4980 { "password_reset_request", "templates/registration/password_reset_request.page.json" },
4981 { "password_reset_sent", "templates/registration/password_reset_sent.page.json" },
4982 { "password_reset_complete", "templates/registration/password_reset_complete.page.json" },
4983 { "password_reset_complete_done", "templates/registration/password_reset_complete_done.page.json" },
4984 { "invite_created", "templates/registration/invite_created.page.json" },
4985 { "invite_accept", "templates/registration/invite_accept.page.json" },
4986 { "invite_accept_done", "templates/registration/invite_accept_done.page.json" },
4987 { "profile", "templates/profile/profile.page.json" },
4988 { "appointments", "templates/appointments/appointments.page.json" },
4989 { "appointment_detail", "templates/appointments/detail/appointment_detail.page.json" },
4990 { "booking_detail", "templates/bookings/booking_detail.page.json" },
4991 { "calendar", "templates/calendar/calendar.page.json" },
4992 { "explore", "templates/explore/explore.page.json" },
4993 { "qr_upload", "templates/qr/qr_upload.page.json" },
4994 { "diagnostics", "templates/appointments/diagnostics.page.json" },
4995 { "business_landing", "templates/businesses/business_landing.page.json" },
4996 { "business_detail_admin", "templates/businesses/business_detail_admin.page.json" },
4997 { "business_dashboard", "templates/businesses/business_dashboard.page.json" },
4998 { "business_calendar", "templates/businesses/business_calendar.page.json" },
4999 { "business_availability_edit", "templates/businesses/business_availability_edit.page.json" },
5000 { "business_create", "templates/businesses/business_create.page.json" },
5001 { "business_edit", "templates/businesses/business_edit.page.json" },
5002 { "service_edit", "templates/businesses/service_edit.page.json" },
5003 { "technician_edit", "templates/businesses/technician_edit.page.json" },
5004 { "generated_admin_manage", "templates/generated_admin/manage.page.json" },
5005 { "generated_admin_edit", "templates/generated_admin/edit.page.json" },
5006 { "service_book_form", "templates/businesses/service_book_form.page.json" },
5007 { "booking_form", "templates/bookings/booking_form.page.json" },
5008 { "bookings", "templates/bookings/bookings.page.json" }
5009 };
5010 size_t index = 0;
5011 if (page_id == NULL || page_id[0] == '\0') {
5012 return NULL;
5013 }
5014 for (index = 0; index < sizeof(entries) / sizeof(entries[0]); index++) {
5015 if (streq(page_id, entries[index].page_id)) {
5016 return entries[index].relative_path;
5017 }
5018 }
5019 return NULL;
5020}
5021
5022char *declared_page_relative_path_dup_runtime(const char *app_root, const char *page_id) {
5023 static const char *patterns[] = {
5024 "templates/%s.page.json",
5025 "templates/%s/%s.page.json"
5026 };
5027 size_t index = 0;
5028 const char *ucal_relative = NULL;
5029 if (app_root == NULL || app_root[0] == '\0' || page_id == NULL || page_id[0] == '\0') {
5030 return NULL;
5031 }
5032 for (index = 0; index < sizeof(patterns) / sizeof(patterns[0]); index++) {
5033 char relative[512];
5034 char *candidate = NULL;
5035 if (index == 0) {
5036 snprintf(relative, sizeof(relative), patterns[index], page_id);
5037 } else {
5038 snprintf(relative, sizeof(relative), patterns[index], page_id, page_id);
5039 }
5040 candidate = app_root_relative_path_dup_runtime(app_root, relative);
5041 if (candidate != NULL && path_exists(candidate)) {
5042 free(candidate);
5043 return strdup(relative);
5044 }
5045 free(candidate);
5046 }
5047 ucal_relative = ucal_declared_page_relative_path_runtime(page_id);
5048 return ucal_relative != NULL ? strdup(ucal_relative) : NULL;
5049}
5050
5051/**
5052 * @brief Resolves one UCAL declared fragment id to its app-relative template path.
5053 * @param fragment_id Declared UCAL fragment id.
5054 * @return Matching relative path, or NULL when the fragment id is unknown.
5055 */
5056static const char *ucal_declared_fragment_relative_path_runtime(const char *fragment_id) {
5057 static const struct {
5058 const char *fragment_id;
5059 const char *relative_path;
5060 } entries[] = {
5061 { "business_logged_out_cta", "templates/businesses/fragments/business_logged_out_cta.html" },
5062 { "business_subscribed_cta", "templates/businesses/fragments/business_subscribed_cta.html" },
5063 { "business_subscribe_cta", "templates/businesses/fragments/business_subscribe_cta.html" },
5064 { "business_rating_form", "templates/businesses/fragments/business_rating_form.html" },
5065 { "business_service_card", "templates/businesses/fragments/business_service_card.html" },
5066 { "business_admin_service_card", "templates/businesses/fragments/business_admin_service_card.html" },
5067 { "business_admin_technician_card", "templates/businesses/fragments/business_admin_technician_card.html" },
5068 { "business_admin_availability_row", "templates/businesses/fragments/business_admin_availability_row.html" },
5069 { "business_admin_public_state_button", "templates/businesses/fragments/business_admin_public_state_button.html" },
5070 { "business_admin_empty_state", "templates/businesses/fragments/business_admin_empty_state.html" },
5071 { "business_dashboard_card", "templates/businesses/fragments/business_dashboard_card.html" },
5072 { "business_landing_image", "templates/businesses/fragments/business_landing_image.html" },
5073 { "business_rating_empty", "templates/businesses/fragments/business_rating_empty.html" },
5074 { "business_services_empty", "templates/businesses/fragments/business_services_empty.html" },
5075 { "business_rating_row", "templates/businesses/fragments/business_rating_row.html" },
5076 { "profile_subscription_row", "templates/profile/fragments/subscription_row.html" },
5077 { "profile_subscriptions_empty", "templates/profile/fragments/subscriptions_empty.html" },
5078 { "managed_appointment_row", "templates/appointments/fragments/managed_appointment_row.html" },
5079 { "customer_appointment_row", "templates/appointments/fragments/customer_appointment_row.html" },
5080 { "calendar_managed_appointment_row", "templates/calendar/fragments/managed_appointment_row.html" },
5081 { "calendar_customer_appointment_row", "templates/calendar/fragments/customer_appointment_row.html" },
5082 { "calendar_booking_row", "templates/calendar/fragments/booking_row.html" },
5083 { "explore_business_card", "templates/explore/fragments/business_card.html" }
5084 };
5085 size_t index = 0;
5086 if (fragment_id == NULL || fragment_id[0] == '\0') {
5087 return NULL;
5088 }
5089 for (index = 0; index < sizeof(entries) / sizeof(entries[0]); index++) {
5090 if (streq(fragment_id, entries[index].fragment_id)) {
5091 return entries[index].relative_path;
5092 }
5093 }
5094 return NULL;
5095}
5096
5097/**
5098 * @brief Joins an app root with one relative template path.
5099 * @param app_root UCAL app root directory.
5100 * @param relative_path Relative path under the app root.
5101 * @return Newly allocated absolute path, or NULL on failure.
5102 */
5103static char *app_root_relative_path_dup_runtime(const char *app_root, const char *relative_path) {
5104 size_t needed = 0;
5105 char *joined = NULL;
5106 if (app_root == NULL || app_root[0] == '\0' || relative_path == NULL || relative_path[0] == '\0') {
5107 return NULL;
5108 }
5109 needed = strlen(app_root) + 1 + strlen(relative_path) + 1;
5110 joined = (char *)malloc(needed);
5111 if (joined == NULL) {
5112 return NULL;
5113 }
5114 snprintf(joined, needed, "%s/%s", app_root, relative_path);
5115 return joined;
5116}
5117
5118/**
5119 * @brief Emits one UCAL declared page contract as JSON.
5120 * @param argc CLI argument count after dispatch trimming.
5121 * @param argv CLI argument vector after dispatch trimming.
5122 * @return Process status code.
5123 */
5124static int handle_template_declared_page_contract_command(int argc, char **argv) {
5125 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
5126 const char *page_id = json_option_value_runtime(argc, argv, "--page-id");
5127 char *relative_path = declared_page_relative_path_dup_runtime(app_root, page_id);
5128 yyjson_doc *doc = NULL;
5129 yyjson_val *root = NULL;
5130 char *spec_path = NULL;
5131 char *template_rel = NULL;
5132 char *template_path = NULL;
5133 char *title = NULL;
5134 char *layout_id = NULL;
5135 int code = 69;
5136 if (relative_path == NULL || app_root == NULL || app_root[0] == '\0') {
5137 free(relative_path);
5138 return 64;
5139 }
5140 spec_path = app_root_relative_path_dup_runtime(app_root, relative_path);
5141 if (spec_path == NULL) {
5142 return 69;
5143 }
5144 doc = native_json_doc_load_file(spec_path);
5145 root = doc != NULL ? yyjson_doc_get_root(doc) : NULL;
5146 if (doc == NULL || root == NULL || !yyjson_is_obj(root)) {
5147 free(spec_path);
5148 free(relative_path);
5150 return 66;
5151 }
5152 template_rel = native_json_obj_get_string_dup(root, "template_file");
5153 title = native_json_obj_get_string_dup(root, "title");
5154 layout_id = native_json_obj_get_string_dup(root, "layout_id");
5155 template_path = app_root_relative_path_dup_runtime(app_root, template_rel != NULL ? template_rel : "");
5156 {
5157 yyjson_mut_doc *out_doc = yyjson_mut_doc_new(NULL);
5158 yyjson_mut_val *out_root = out_doc != NULL ? yyjson_mut_obj(out_doc) : NULL;
5159 if (out_doc == NULL || out_root == NULL
5160 || !yyjson_mut_obj_add_strcpy(out_doc, out_root, "spec_path", spec_path)
5161 || !yyjson_mut_obj_add_strcpy(out_doc, out_root, "template_path", template_path != NULL ? template_path : "")
5162 || !yyjson_mut_obj_add_strcpy(out_doc, out_root, "title", title != NULL ? title : "")
5163 || !yyjson_mut_obj_add_strcpy(out_doc, out_root, "layout_id", layout_id != NULL ? layout_id : "")) {
5164 yyjson_mut_doc_free(out_doc);
5165 code = 69;
5166 } else {
5167 code = emit_compact_json_mut_value_runtime(out_root);
5168 yyjson_mut_doc_free(out_doc);
5169 }
5170 }
5171 free(spec_path);
5172 free(relative_path);
5173 free(template_rel);
5174 free(template_path);
5175 free(title);
5176 free(layout_id);
5178 return code;
5179}
5180
5181/**
5182 * @brief Emits one UCAL declared fragment template path as text.
5183 * @param argc CLI argument count after dispatch trimming.
5184 * @param argv CLI argument vector after dispatch trimming.
5185 * @return Process status code.
5186 */
5187static int handle_template_declared_fragment_path_command(int argc, char **argv) {
5188 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
5189 const char *fragment_id = json_option_value_runtime(argc, argv, "--fragment-id");
5190 const char *relative_path = ucal_declared_fragment_relative_path_runtime(fragment_id);
5191 char *full_path = NULL;
5192 int code = 0;
5193 if (relative_path == NULL || app_root == NULL || app_root[0] == '\0') {
5194 return 64;
5195 }
5196 full_path = app_root_relative_path_dup_runtime(app_root, relative_path);
5197 if (full_path == NULL) {
5198 return 69;
5199 }
5200 code = emit_text_line_runtime(full_path);
5201 free(full_path);
5202 return code;
5203}
5204
5205static char *day_of_week_options_html_dup_runtime(const char *selected_day) {
5206 static const char *days[] = { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" };
5207 TextBufferRuntime buffer = {0};
5208 size_t index = 0;
5209 for (index = 0; index < (sizeof(days) / sizeof(days[0])); index++) {
5210 if (!text_buffer_append_format_runtime(&buffer, "<option value=\"%s\"%s>%s</option>", days[index], (selected_day != NULL && streq(selected_day, days[index])) ? " selected" : "", days[index])) {
5211 text_buffer_free_runtime(&buffer);
5212 return NULL;
5213 }
5214 }
5215 return text_buffer_take_runtime(&buffer);
5216}
5217
5218static int handle_template_render_path_command(int argc, char **argv) {
5219 const char *template_path = json_option_value_runtime(argc, argv, "--template-path");
5220 const char *params_json = json_option_value_runtime(argc, argv, "--params-json");
5221 yyjson_doc *params_doc = NULL;
5222 yyjson_val *params_root = NULL;
5223 int code = 0;
5224 if (template_path == NULL || params_json == NULL) {
5225 return 64;
5226 }
5227 params_root = load_root_from_text_runtime(params_json, &params_doc);
5228 if (params_root == NULL || !yyjson_is_obj(params_root)) {
5229 native_json_doc_free(params_doc);
5230 return 64;
5231 }
5232 {
5233 char *rendered = render_path_template_text_dup_runtime(template_path, params_root);
5234 if (rendered == NULL) {
5235 native_json_doc_free(params_doc);
5236 return 64;
5237 }
5238 fputs(rendered, stdout);
5239 free(rendered);
5240 code = 0;
5241 }
5242 native_json_doc_free(params_doc);
5243 return code;
5244}
5245
5246static int handle_template_render_file_command(int argc, char **argv) {
5247 const char *template_file = json_option_value_runtime(argc, argv, "--template-file");
5248 const char *params_json = json_option_value_runtime(argc, argv, "--params-json");
5249 char *template_text = NULL;
5250 yyjson_doc *params_doc = NULL;
5251 yyjson_val *params_root = NULL;
5252 int code = 0;
5253 if (template_file == NULL || template_file[0] == '\0' || params_json == NULL) {
5254 return 64;
5255 }
5256 template_text = read_file_text(template_file);
5257 if (template_text == NULL) {
5258 return 66;
5259 }
5260 params_root = load_root_from_text_runtime(params_json, &params_doc);
5261 if (params_root == NULL || !yyjson_is_obj(params_root)) {
5262 free(template_text);
5263 native_json_doc_free(params_doc);
5264 return 64;
5265 }
5266 code = emit_template_text_runtime(template_text, params_root);
5267 free(template_text);
5268 native_json_doc_free(params_doc);
5269 return code;
5270}
5271
5272char *apply_base_path_html_dup_runtime(const char *html, const char *base_path) {
5273 static const char *prefixes[] = {"href=\"/", "src=\"/", "action=\"/", "value=\"/"};
5274 TextBufferRuntime buffer = {0};
5275 size_t index = 0;
5276 if (html == NULL) {
5277 return NULL;
5278 }
5279 if (base_path == NULL || base_path[0] == '\0' || streq(base_path, "/")) {
5280 return strdup(html);
5281 }
5282 while (html[index] != '\0') {
5283 size_t prefix_index = 0;
5284 int matched = 0;
5285 for (prefix_index = 0; prefix_index < sizeof(prefixes) / sizeof(prefixes[0]); prefix_index++) {
5286 const char *prefix = prefixes[prefix_index];
5287 size_t prefix_length = strlen(prefix);
5288 if (strncmp(html + index, prefix, prefix_length) == 0) {
5289 if (!text_buffer_append_format_runtime(&buffer, "%.*s", (int)(prefix_length - 1), prefix)
5290 || !text_buffer_append_text_runtime(&buffer, base_path)
5291 || !text_buffer_append_text_runtime(&buffer, "/")) {
5292 text_buffer_free_runtime(&buffer);
5293 return NULL;
5294 }
5295 index += prefix_length;
5296 matched = 1;
5297 break;
5298 }
5299 }
5300 if (matched) {
5301 continue;
5302 }
5303 if (!text_buffer_append_format_runtime(&buffer, "%c", html[index])) {
5304 text_buffer_free_runtime(&buffer);
5305 return NULL;
5306 }
5307 index++;
5308 }
5309 return text_buffer_take_runtime(&buffer);
5310}
5311
5312static int handle_template_apply_base_path_command(int argc, char **argv) {
5313 const char *html = json_option_value_runtime(argc, argv, "--html");
5314 const char *base_path = json_option_value_runtime(argc, argv, "--base-path");
5315 char *result = NULL;
5316 int code = 69;
5317 if (html == NULL) {
5318 return 64;
5319 }
5320 result = apply_base_path_html_dup_runtime(html, base_path != NULL ? base_path : "");
5321 if (result != NULL) {
5322 fputs(result, stdout);
5323 code = 0;
5324 }
5325 free(result);
5326 return code;
5327}
5328
5329char *html_escaped_dup_runtime(const char *text) {
5330 TextBufferRuntime buffer = {0};
5331 if (!text_buffer_append_html_escaped_runtime(&buffer, text != NULL ? text : "")) {
5332 text_buffer_free_runtime(&buffer);
5333 return NULL;
5334 }
5335 return text_buffer_take_runtime(&buffer);
5336}
5337
5338char *error_alert_html_dup_runtime(const char *message) {
5339 TextBufferRuntime buffer = {0};
5340 if (message == NULL || message[0] == '\0') {
5341 return strdup("");
5342 }
5343 if (!text_buffer_append_text_runtime(&buffer, "<div class=\"alert alert-error\" style=\"margin-top:1rem\">")
5344 || !text_buffer_append_html_escaped_runtime(&buffer, message)
5345 || !text_buffer_append_text_runtime(&buffer, "</div>")) {
5346 text_buffer_free_runtime(&buffer);
5347 return NULL;
5348 }
5349 return text_buffer_take_runtime(&buffer);
5350}
5351
5352char *hidden_next_input_html_dup_runtime(const char *next_path) {
5353 TextBufferRuntime buffer = {0};
5354 if (next_path == NULL || next_path[0] == '\0') {
5355 return strdup("");
5356 }
5357 if (!text_buffer_append_text_runtime(&buffer, "<input type=\"hidden\" name=\"next\" value=\"")
5358 || !text_buffer_append_html_escaped_runtime(&buffer, next_path)
5359 || !text_buffer_append_text_runtime(&buffer, "\" />")) {
5360 text_buffer_free_runtime(&buffer);
5361 return NULL;
5362 }
5363 return text_buffer_take_runtime(&buffer);
5364}
5365
5367 const char *error_html,
5368 const char *next_input,
5369 const char *account_type_options_html,
5370 const char *preview_html,
5371 const char *reset_post_path,
5372 const char *invite_email,
5373 const char *invite_path,
5374 const char *return_path,
5375 const char *invite_kind,
5376 const char *target_html,
5377 const char *form_action
5378) {
5380 yyjson_mut_val *root = doc != NULL ? yyjson_mut_obj(doc) : NULL;
5381 int code = 69;
5382 if (doc == NULL || root == NULL) {
5384 return 69;
5385 }
5386 yyjson_mut_doc_set_root(doc, root);
5387 yyjson_mut_obj_add_strcpy(doc, root, "error_html", error_html != NULL ? error_html : "");
5388 yyjson_mut_obj_add_strcpy(doc, root, "next_input", next_input != NULL ? next_input : "");
5389 yyjson_mut_obj_add_strcpy(doc, root, "account_type_options_html", account_type_options_html != NULL ? account_type_options_html : "");
5390 yyjson_mut_obj_add_strcpy(doc, root, "preview_html", preview_html != NULL ? preview_html : "");
5391 yyjson_mut_obj_add_strcpy(doc, root, "reset_post_path", reset_post_path != NULL ? reset_post_path : "");
5392 yyjson_mut_obj_add_strcpy(doc, root, "invite_email", invite_email != NULL ? invite_email : "");
5393 yyjson_mut_obj_add_strcpy(doc, root, "invite_path", invite_path != NULL ? invite_path : "");
5394 yyjson_mut_obj_add_strcpy(doc, root, "return_path", return_path != NULL ? return_path : "");
5395 yyjson_mut_obj_add_strcpy(doc, root, "invite_kind", invite_kind != NULL ? invite_kind : "");
5396 yyjson_mut_obj_add_strcpy(doc, root, "target_html", target_html != NULL ? target_html : "");
5397 yyjson_mut_obj_add_strcpy(doc, root, "form_action", form_action != NULL ? form_action : "");
5400 return code;
5401}
5402
5403char *language_options_html_dup_runtime(const char *current_lang) {
5404 static const char *const language_codes[] = {"en", "ja", "es", "fr", "de", "pt"};
5405 static const char *const language_labels[] = {"English", "日本語", "Español", "Français", "Deutsch", "Português"};
5406 TextBufferRuntime buffer = {0};
5407 size_t index = 0;
5408 for (index = 0; index < sizeof(language_codes) / sizeof(language_codes[0]); index++) {
5409 const char *selected = streq(current_lang != NULL ? current_lang : "en", language_codes[index]) ? " selected" : "";
5410 if (!text_buffer_append_format_runtime(&buffer, "<option value=\"%s\"%s>%s</option>", language_codes[index], selected, language_labels[index])) {
5411 text_buffer_free_runtime(&buffer);
5412 return NULL;
5413 }
5414 }
5415 return text_buffer_take_runtime(&buffer);
5416}
5417
5419 const char *app_root,
5420 const char *base_path,
5421 const char *layout_id,
5422 const char *title,
5423 const char *body,
5424 const char *current_lang,
5425 const char *current_request_target,
5426 int has_user,
5427 int managed_business_count
5428) {
5429 const char *effective_layout_id = layout_id != NULL && layout_id[0] != '\0' ? layout_id : "default_shell";
5431 yyjson_mut_val *root = doc != NULL ? yyjson_mut_obj(doc) : NULL;
5432 yyjson_doc *render_params_doc = NULL;
5433 yyjson_val *render_params_root = NULL;
5434 char *render_params_json = NULL;
5435 char *language_options = NULL;
5436 char *escaped_request_target = NULL;
5437 char *nav_language_form = NULL;
5438 const char *nav_home_path = "/explore/";
5439 const char *nav_primary = "";
5440 const char *nav_account = "";
5441 TextBufferRuntime language_form_buffer = {0};
5442 TextBufferRuntime nav_account_buffer = {0};
5443 char *layout_file = NULL;
5444 char *rendered = NULL;
5445 char *scoped = NULL;
5446 char *result = NULL;
5447 if (app_root == NULL || app_root[0] == '\0' || doc == NULL || root == NULL || title == NULL || body == NULL) {
5449 return NULL;
5450 }
5451 language_options = language_options_html_dup_runtime(current_lang != NULL && current_lang[0] != '\0' ? current_lang : "en");
5452 escaped_request_target = html_escaped_dup_runtime(current_request_target != NULL && current_request_target[0] != '\0' ? current_request_target : "/");
5453 if (language_options == NULL || escaped_request_target == NULL) {
5454 goto layout_page_cleanup;
5455 }
5456 if (!text_buffer_append_text_runtime(&language_form_buffer, "<form action=\"/i18n/setlang/\" method=\"post\" class=\"language-form\"><input name=\"next\" type=\"hidden\" value=\"")
5457 || !text_buffer_append_text_runtime(&language_form_buffer, escaped_request_target)
5458 || !text_buffer_append_text_runtime(&language_form_buffer, "\" /><label for=\"language-select\" class=\"sr-only\">Language</label><select id=\"language-select\" name=\"language\" onchange=\"this.form.submit()\" aria-label=\"Language\">")
5459 || !text_buffer_append_text_runtime(&language_form_buffer, language_options)
5460 || !text_buffer_append_text_runtime(&language_form_buffer, "</select></form>")) {
5461 goto layout_page_cleanup;
5462 }
5463 nav_language_form = text_buffer_take_runtime(&language_form_buffer);
5464 if (nav_language_form == NULL) {
5465 goto layout_page_cleanup;
5466 }
5467 if (has_user > 0) {
5468 nav_home_path = "/";
5469 if (managed_business_count > 0) {
5470 nav_primary = "<a href=\"/appointments/\">Manage bookings</a> <a href=\"/businesses/\">My Businesses</a>";
5471 } else if (streq(effective_layout_id, "account_shell")) {
5472 nav_primary = "<a href=\"/appointments/\">Appointments</a>";
5473 nav_home_path = "/explore/";
5474 } else if (streq(effective_layout_id, "public_catalog_shell") || streq(effective_layout_id, "public_business_shell")) {
5475 nav_primary = "<a href=\"/appointments/\">Appointments</a>";
5476 nav_home_path = "/explore/";
5477 }
5478 if (!text_buffer_append_text_runtime(&nav_account_buffer, nav_language_form)
5479 || !text_buffer_append_text_runtime(&nav_account_buffer, " <a href=\"/profile/\">Profile</a><form method=\"post\" action=\"/accounts/logout/\" style=\"display:inline\"><button type=\"submit\" class=\"btn btn-secondary\">Log out</button></form>")) {
5480 goto layout_page_cleanup;
5481 }
5482 nav_account = nav_account_buffer.text != NULL ? nav_account_buffer.text : "";
5483 } else {
5484 nav_home_path = "/accounts/login/";
5485 nav_primary = "<a href=\"/accounts/login/\">Log in</a> <a href=\"/accounts/register/\">Sign Up</a>";
5486 nav_account = nav_language_form;
5487 }
5488 yyjson_mut_doc_set_root(doc, root);
5489 yyjson_mut_obj_add_strcpy(doc, root, "current_lang", current_lang != NULL && current_lang[0] != '\0' ? current_lang : "en");
5490 yyjson_mut_obj_add_strcpy(doc, root, "title", title);
5491 yyjson_mut_obj_add_strcpy(doc, root, "nav_home_path", nav_home_path);
5492 yyjson_mut_obj_add_strcpy(doc, root, "nav_primary", nav_primary);
5493 yyjson_mut_obj_add_strcpy(doc, root, "nav_account", nav_account != NULL ? nav_account : "");
5494 yyjson_mut_obj_add_strcpy(doc, root, "body", body);
5495 render_params_json = yyjson_mut_write(doc, YYJSON_WRITE_NOFLAG, NULL);
5496 render_params_root = render_params_json != NULL ? load_root_from_text_runtime(render_params_json, &render_params_doc) : NULL;
5497 layout_file = join_base_relative_path_dup_runtime(app_root, "templates/layout/base.html");
5498 rendered = (layout_file != NULL && render_params_root != NULL) ? render_file_template_dup_runtime(layout_file, render_params_root) : NULL;
5499 scoped = apply_base_path_html_dup_runtime(rendered, base_path != NULL ? base_path : "");
5500 if (scoped != NULL) {
5501 result = strdup(scoped);
5502 }
5503layout_page_cleanup:
5504 text_buffer_free_runtime(&language_form_buffer);
5505 text_buffer_free_runtime(&nav_account_buffer);
5506 free(language_options);
5507 free(escaped_request_target);
5508 free(nav_language_form);
5510 native_json_doc_free(render_params_doc);
5511 free(render_params_json);
5512 free(layout_file);
5513 free(rendered);
5514 free(scoped);
5515 return result;
5516}
5517
5518static int handle_template_layout_page_html_command(int argc, char **argv) {
5519 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
5520 const char *base_path = json_option_value_runtime(argc, argv, "--base-path");
5521 const char *layout_id = json_option_value_runtime(argc, argv, "--layout-id");
5522 const char *current_lang = json_option_value_runtime(argc, argv, "--current-lang");
5523 const char *title = json_option_value_runtime(argc, argv, "--title");
5524 const char *body = json_option_value_runtime(argc, argv, "--body");
5525 const char *current_request_target = json_option_value_runtime(argc, argv, "--current-request-target");
5526 long has_user = strtol(json_option_value_runtime(argc, argv, "--has-user") != NULL ? json_option_value_runtime(argc, argv, "--has-user") : "0", NULL, 10);
5527 long managed_business_count = strtol(json_option_value_runtime(argc, argv, "--managed-business-count") != NULL ? json_option_value_runtime(argc, argv, "--managed-business-count") : "0", NULL, 10);
5528 const char *effective_layout_id = layout_id != NULL && layout_id[0] != '\0' ? layout_id : "default_shell";
5530 yyjson_mut_val *root = doc != NULL ? yyjson_mut_obj(doc) : NULL;
5531 yyjson_doc *render_params_doc = NULL;
5532 yyjson_val *render_params_root = NULL;
5533 char *render_params_json = NULL;
5534 char *language_options = NULL;
5535 char *escaped_request_target = NULL;
5536 char *nav_language_form = NULL;
5537 const char *nav_home_path = "/explore/";
5538 const char *nav_primary = "";
5539 const char *nav_account = "";
5540 TextBufferRuntime language_form_buffer = {0};
5541 TextBufferRuntime nav_account_buffer = {0};
5542 char *layout_file = NULL;
5543 char *rendered = NULL;
5544 char *scoped = NULL;
5545 int code = 69;
5546 if (app_root == NULL || app_root[0] == '\0' || doc == NULL || root == NULL || title == NULL || body == NULL) {
5548 return 64;
5549 }
5550 language_options = language_options_html_dup_runtime(current_lang != NULL && current_lang[0] != '\0' ? current_lang : "en");
5551 escaped_request_target = html_escaped_dup_runtime(current_request_target != NULL && current_request_target[0] != '\0' ? current_request_target : "/");
5552 if (language_options == NULL || escaped_request_target == NULL) {
5553 goto layout_page_html_cleanup;
5554 }
5555 if (!text_buffer_append_text_runtime(&language_form_buffer, "<form action=\"/i18n/setlang/\" method=\"post\" class=\"language-form\"><input name=\"next\" type=\"hidden\" value=\"")
5556 || !text_buffer_append_text_runtime(&language_form_buffer, escaped_request_target)
5557 || !text_buffer_append_text_runtime(&language_form_buffer, "\" /><label for=\"language-select\" class=\"sr-only\">Language</label><select id=\"language-select\" name=\"language\" onchange=\"this.form.submit()\" aria-label=\"Language\">")
5558 || !text_buffer_append_text_runtime(&language_form_buffer, language_options)
5559 || !text_buffer_append_text_runtime(&language_form_buffer, "</select></form>")) {
5560 goto layout_page_html_cleanup;
5561 }
5562 nav_language_form = text_buffer_take_runtime(&language_form_buffer);
5563 if (nav_language_form == NULL) {
5564 goto layout_page_html_cleanup;
5565 }
5566 if (has_user > 0) {
5567 nav_home_path = "/";
5568 if (managed_business_count > 0) {
5569 nav_primary = "<a href=\"/appointments/\">Manage bookings</a> <a href=\"/businesses/\">My Businesses</a>";
5570 } else if (streq(effective_layout_id, "account_shell")) {
5571 nav_primary = "<a href=\"/appointments/\">Appointments</a>";
5572 nav_home_path = "/explore/";
5573 } else if (streq(effective_layout_id, "public_catalog_shell") || streq(effective_layout_id, "public_business_shell")) {
5574 nav_primary = "<a href=\"/appointments/\">Appointments</a>";
5575 nav_home_path = "/explore/";
5576 }
5577 if (!text_buffer_append_text_runtime(&nav_account_buffer, nav_language_form)
5578 || !text_buffer_append_text_runtime(&nav_account_buffer, " <a href=\"/profile/\">Profile</a><form method=\"post\" action=\"/accounts/logout/\" style=\"display:inline\"><button type=\"submit\" class=\"btn btn-secondary\">Log out</button></form>")) {
5579 goto layout_page_html_cleanup;
5580 }
5581 nav_account = nav_account_buffer.text != NULL ? nav_account_buffer.text : "";
5582 } else {
5583 nav_home_path = "/accounts/login/";
5584 nav_primary = "<a href=\"/accounts/login/\">Log in</a> <a href=\"/accounts/register/\">Sign Up</a>";
5585 nav_account = nav_language_form;
5586 }
5587 yyjson_mut_doc_set_root(doc, root);
5588 yyjson_mut_obj_add_strcpy(doc, root, "current_lang", current_lang != NULL && current_lang[0] != '\0' ? current_lang : "en");
5589 yyjson_mut_obj_add_strcpy(doc, root, "title", title);
5590 yyjson_mut_obj_add_strcpy(doc, root, "nav_home_path", nav_home_path);
5591 yyjson_mut_obj_add_strcpy(doc, root, "nav_primary", nav_primary);
5592 yyjson_mut_obj_add_strcpy(doc, root, "nav_account", nav_account != NULL ? nav_account : "");
5593 yyjson_mut_obj_add_strcpy(doc, root, "body", body);
5594 render_params_json = yyjson_mut_write(doc, YYJSON_WRITE_NOFLAG, NULL);
5595 render_params_root = render_params_json != NULL ? load_root_from_text_runtime(render_params_json, &render_params_doc) : NULL;
5596 layout_file = join_base_relative_path_dup_runtime(app_root, "templates/layout/base.html");
5597 rendered = (layout_file != NULL && render_params_root != NULL) ? render_file_template_dup_runtime(layout_file, render_params_root) : NULL;
5598 scoped = apply_base_path_html_dup_runtime(rendered, base_path != NULL ? base_path : "");
5599 if (scoped != NULL) {
5600 fputs(scoped, stdout);
5601 code = 0;
5602 }
5603layout_page_html_cleanup:
5604 text_buffer_free_runtime(&language_form_buffer);
5605 text_buffer_free_runtime(&nav_account_buffer);
5606 free(language_options);
5607 free(escaped_request_target);
5608 free(nav_language_form);
5610 native_json_doc_free(render_params_doc);
5611 free(render_params_json);
5612 free(layout_file);
5613 free(rendered);
5614 free(scoped);
5615 return code;
5616}
5617
5618static int handle_template_layout_page_json_command(int argc, char **argv) {
5619 const char *layout_id = json_option_value_runtime(argc, argv, "--layout-id");
5620 const char *current_lang = json_option_value_runtime(argc, argv, "--current-lang");
5621 const char *title = json_option_value_runtime(argc, argv, "--title");
5622 const char *body = json_option_value_runtime(argc, argv, "--body");
5623 const char *current_request_target = json_option_value_runtime(argc, argv, "--current-request-target");
5624 long has_user = strtol(json_option_value_runtime(argc, argv, "--has-user") != NULL ? json_option_value_runtime(argc, argv, "--has-user") : "0", NULL, 10);
5625 long managed_business_count = strtol(json_option_value_runtime(argc, argv, "--managed-business-count") != NULL ? json_option_value_runtime(argc, argv, "--managed-business-count") : "0", NULL, 10);
5626 const char *effective_layout_id = layout_id != NULL && layout_id[0] != '\0' ? layout_id : "default_shell";
5628 yyjson_mut_val *root = doc != NULL ? yyjson_mut_obj(doc) : NULL;
5629 char *language_options = NULL;
5630 char *escaped_request_target = NULL;
5631 char *nav_language_form = NULL;
5632 const char *nav_home_path = "/explore/";
5633 const char *nav_primary = "";
5634 const char *nav_account = "";
5635 TextBufferRuntime language_form_buffer = {0};
5636 TextBufferRuntime nav_account_buffer = {0};
5637 int code = 69;
5638 if (doc == NULL || root == NULL || title == NULL || body == NULL) {
5640 return 69;
5641 }
5642 language_options = language_options_html_dup_runtime(current_lang != NULL && current_lang[0] != '\0' ? current_lang : "en");
5643 escaped_request_target = html_escaped_dup_runtime(current_request_target != NULL && current_request_target[0] != '\0' ? current_request_target : "/");
5644 if (language_options == NULL || escaped_request_target == NULL) {
5645 goto layout_page_cleanup;
5646 }
5647 if (!text_buffer_append_text_runtime(&language_form_buffer, "<form action=\"/i18n/setlang/\" method=\"post\" class=\"language-form\"><input name=\"next\" type=\"hidden\" value=\"")
5648 || !text_buffer_append_text_runtime(&language_form_buffer, escaped_request_target)
5649 || !text_buffer_append_text_runtime(&language_form_buffer, "\" /><label for=\"language-select\" class=\"sr-only\">Language</label><select id=\"language-select\" name=\"language\" onchange=\"this.form.submit()\" aria-label=\"Language\">")
5650 || !text_buffer_append_text_runtime(&language_form_buffer, language_options)
5651 || !text_buffer_append_text_runtime(&language_form_buffer, "</select></form>")) {
5652 goto layout_page_cleanup;
5653 }
5654 nav_language_form = text_buffer_take_runtime(&language_form_buffer);
5655 if (nav_language_form == NULL) {
5656 goto layout_page_cleanup;
5657 }
5658 if (has_user > 0) {
5659 nav_home_path = "/";
5660 if (managed_business_count > 0) {
5661 nav_primary = "<a href=\"/appointments/\">Manage bookings</a> <a href=\"/businesses/\">My Businesses</a>";
5662 } else if (streq(effective_layout_id, "account_shell")) {
5663 nav_primary = "<a href=\"/appointments/\">Appointments</a>";
5664 nav_home_path = "/explore/";
5665 } else if (streq(effective_layout_id, "public_catalog_shell") || streq(effective_layout_id, "public_business_shell")) {
5666 nav_primary = "<a href=\"/appointments/\">Appointments</a>";
5667 nav_home_path = "/explore/";
5668 }
5669 if (!text_buffer_append_text_runtime(&nav_account_buffer, nav_language_form)
5670 || !text_buffer_append_text_runtime(&nav_account_buffer, " <a href=\"/profile/\">Profile</a><form method=\"post\" action=\"/accounts/logout/\" style=\"display:inline\"><button type=\"submit\" class=\"btn btn-secondary\">Log out</button></form>")) {
5671 goto layout_page_cleanup;
5672 }
5673 nav_account = nav_account_buffer.text != NULL ? nav_account_buffer.text : "";
5674 } else {
5675 nav_home_path = "/accounts/login/";
5676 nav_primary = "<a href=\"/accounts/login/\">Log in</a> <a href=\"/accounts/register/\">Sign Up</a>";
5677 nav_account = nav_language_form;
5678 }
5679 yyjson_mut_doc_set_root(doc, root);
5680 yyjson_mut_obj_add_strcpy(doc, root, "current_lang", current_lang != NULL && current_lang[0] != '\0' ? current_lang : "en");
5681 yyjson_mut_obj_add_strcpy(doc, root, "title", title);
5682 yyjson_mut_obj_add_strcpy(doc, root, "nav_home_path", nav_home_path);
5683 yyjson_mut_obj_add_strcpy(doc, root, "nav_primary", nav_primary);
5684 yyjson_mut_obj_add_strcpy(doc, root, "nav_account", nav_account != NULL ? nav_account : "");
5685 yyjson_mut_obj_add_strcpy(doc, root, "body", body);
5687layout_page_cleanup:
5688 text_buffer_free_runtime(&language_form_buffer);
5689 text_buffer_free_runtime(&nav_account_buffer);
5690 free(language_options);
5691 free(escaped_request_target);
5692 free(nav_language_form);
5694 return code;
5695}
5696
5697static int handle_template_login_page_json_command(int argc, char **argv) {
5698 const char *error_message = json_option_value_runtime(argc, argv, "--error-message");
5699 const char *next_path = json_option_value_runtime(argc, argv, "--next-path");
5700 char *error_html = error_alert_html_dup_runtime(error_message != NULL ? error_message : "");
5701 char *next_input = hidden_next_input_html_dup_runtime(next_path != NULL ? next_path : "");
5702 int code = (error_html != NULL && next_input != NULL)
5703 ? emit_template_page_params_json_runtime(error_html, next_input, "", "", "", "", "", "", "", "", "")
5704 : 69;
5705 free(error_html);
5706 free(next_input);
5707 return code;
5708}
5709
5710static int handle_template_register_page_json_command(int argc, char **argv) {
5711 const char *error_message = json_option_value_runtime(argc, argv, "--error-message");
5712 const char *next_path = json_option_value_runtime(argc, argv, "--next-path");
5713 const char *account_type_options_html = json_option_value_runtime(argc, argv, "--account-type-options-html");
5714 char *error_html = error_alert_html_dup_runtime(error_message != NULL ? error_message : "");
5715 char *next_input = hidden_next_input_html_dup_runtime(next_path != NULL ? next_path : "");
5716 int code = (error_html != NULL && next_input != NULL)
5717 ? emit_template_page_params_json_runtime(error_html, next_input, account_type_options_html != NULL ? account_type_options_html : "", "", "", "", "", "", "", "", "")
5718 : 69;
5719 free(error_html);
5720 free(next_input);
5721 return code;
5722}
5723
5725 const char *error_message = json_option_value_runtime(argc, argv, "--error-message");
5726 char *error_html = error_alert_html_dup_runtime(error_message != NULL ? error_message : "");
5727 int code = error_html != NULL ? emit_template_page_params_json_runtime(error_html, "", "", "", "", "", "", "", "", "", "") : 69;
5728 free(error_html);
5729 return code;
5730}
5731
5733 const char *preview_path = json_option_value_runtime(argc, argv, "--preview-path");
5734 TextBufferRuntime buffer = {0};
5735 char *preview_html = NULL;
5736 int code = 69;
5737 if (preview_path == NULL || preview_path[0] == '\0') {
5738 return emit_template_page_params_json_runtime("", "", "", "", "", "", "", "", "", "", "");
5739 }
5740 if (!text_buffer_append_text_runtime(&buffer, "<p>Local runtime preview link:</p><p><a href=\"")
5741 || !text_buffer_append_html_escaped_runtime(&buffer, preview_path)
5742 || !text_buffer_append_text_runtime(&buffer, "\"><code>")
5743 || !text_buffer_append_html_escaped_runtime(&buffer, preview_path)
5744 || !text_buffer_append_text_runtime(&buffer, "</code></a></p>")) {
5745 text_buffer_free_runtime(&buffer);
5746 return 69;
5747 }
5748 preview_html = text_buffer_take_runtime(&buffer);
5749 code = emit_template_page_params_json_runtime("", "", "", preview_html, "", "", "", "", "", "", "");
5750 free(preview_html);
5751 return code;
5752}
5753
5755 const char *error_message = json_option_value_runtime(argc, argv, "--error-message");
5756 const char *reset_post_path = json_option_value_runtime(argc, argv, "--reset-post-path");
5757 char *error_html = error_alert_html_dup_runtime(error_message != NULL ? error_message : "");
5758 char *escaped_reset_post_path = html_escaped_dup_runtime(reset_post_path != NULL ? reset_post_path : "");
5759 int code = (error_html != NULL && escaped_reset_post_path != NULL)
5760 ? emit_template_page_params_json_runtime(error_html, "", "", "", escaped_reset_post_path, "", "", "", "", "", "")
5761 : 69;
5762 free(error_html);
5763 free(escaped_reset_post_path);
5764 return code;
5765}
5766
5767static int handle_template_invite_created_page_json_command(int argc, char **argv) {
5768 const char *invite_email = json_option_value_runtime(argc, argv, "--invite-email");
5769 const char *invite_path = json_option_value_runtime(argc, argv, "--invite-path");
5770 const char *return_path = json_option_value_runtime(argc, argv, "--return-path");
5771 char *escaped_invite_email = html_escaped_dup_runtime(invite_email != NULL ? invite_email : "");
5772 char *escaped_invite_path = html_escaped_dup_runtime(invite_path != NULL ? invite_path : "");
5773 char *escaped_return_path = html_escaped_dup_runtime(return_path != NULL ? return_path : "");
5774 int code = (escaped_invite_email != NULL && escaped_invite_path != NULL && escaped_return_path != NULL)
5775 ? emit_template_page_params_json_runtime("", "", "", "", "", escaped_invite_email, escaped_invite_path, escaped_return_path, "", "", "")
5776 : 69;
5777 free(escaped_invite_email);
5778 free(escaped_invite_path);
5779 free(escaped_return_path);
5780 return code;
5781}
5782
5783static int handle_template_invite_accept_page_json_command(int argc, char **argv) {
5784 const char *invite_kind = json_option_value_runtime(argc, argv, "--invite-kind");
5785 const char *target_business_name = json_option_value_runtime(argc, argv, "--target-business-name");
5786 const char *error_message = json_option_value_runtime(argc, argv, "--error-message");
5787 const char *invite_email = json_option_value_runtime(argc, argv, "--invite-email");
5788 const char *form_action = json_option_value_runtime(argc, argv, "--form-action");
5789 TextBufferRuntime target_buffer = {0};
5790 char *target_html = NULL;
5791 char *error_html = error_alert_html_dup_runtime(error_message != NULL ? error_message : "");
5792 char *escaped_invite_email = html_escaped_dup_runtime(invite_email != NULL ? invite_email : "");
5793 char *escaped_invite_kind = html_escaped_dup_runtime(invite_kind != NULL ? invite_kind : "account_invite");
5794 char *escaped_form_action = html_escaped_dup_runtime(form_action != NULL ? form_action : "");
5795 int code = 69;
5796 if (error_html == NULL || escaped_invite_email == NULL || escaped_invite_kind == NULL || escaped_form_action == NULL) {
5797 free(error_html);
5798 free(escaped_invite_email);
5799 free(escaped_invite_kind);
5800 free(escaped_form_action);
5801 return 69;
5802 }
5803 if (target_business_name != NULL && target_business_name[0] != '\0') {
5804 if (!text_buffer_append_text_runtime(&target_buffer, "<p>This invite is associated with <strong>")
5805 || !text_buffer_append_html_escaped_runtime(&target_buffer, target_business_name)
5806 || !text_buffer_append_text_runtime(&target_buffer, "</strong>.</p>")) {
5807 text_buffer_free_runtime(&target_buffer);
5808 free(error_html);
5809 free(escaped_invite_email);
5810 free(escaped_invite_kind);
5811 free(escaped_form_action);
5812 return 69;
5813 }
5814 target_html = text_buffer_take_runtime(&target_buffer);
5815 }
5817 error_html,
5818 "",
5819 "",
5820 "",
5821 "",
5822 escaped_invite_email,
5823 "",
5824 "",
5825 escaped_invite_kind,
5826 target_html != NULL ? target_html : "",
5827 escaped_form_action
5828 );
5829 free(error_html);
5830 free(escaped_invite_email);
5831 free(escaped_invite_kind);
5832 free(escaped_form_action);
5833 free(target_html);
5834 return code;
5835}
5836
5837static int handle_validation_empty_report_command(int argc, char **argv) {
5838 const char *form_id = json_option_value_runtime(argc, argv, "--form-id");
5839 const char *action_id = json_option_value_runtime(argc, argv, "--action-id");
5840 const char *actor_scope = json_option_value_runtime(argc, argv, "--actor-scope");
5841 const char *tenant_scope = json_option_value_runtime(argc, argv, "--tenant-scope");
5842 fprintf(stdout, "{\"schema\":\"pharos.validation.report.v1\",\"report_id\":\"pharos.validation.report.v1\",\"form_id\":");
5843 emit_json_string_runtime(form_id != NULL ? form_id : "");
5844 fprintf(stdout, ",\"action_id\":");
5845 emit_json_string_runtime(action_id != NULL ? action_id : "");
5846 fprintf(stdout, ",\"field_errors\":[],\"form_errors\":[],\"actor_scope\":");
5847 emit_json_string_runtime(actor_scope != NULL ? actor_scope : "anonymous");
5848 fprintf(stdout, ",\"tenant_scope\":");
5849 emit_json_string_runtime(tenant_scope != NULL ? tenant_scope : "global");
5850 fputc('}', stdout);
5851 return 0;
5852}
5853
5854static int append_validation_error_runtime(const char *report_json, const char *array_name, const char *field_id, const char *rule_id, const char *code_text, const char *message) {
5855 yyjson_doc *doc = NULL;
5856 yyjson_val *root = load_root_from_text_runtime(report_json, &doc);
5857 yyjson_mut_doc *mut_doc = NULL;
5858 yyjson_mut_val *mut_root = NULL;
5859 yyjson_mut_val *errors = NULL;
5860 yyjson_mut_val *entry = NULL;
5861 int code = 69;
5862 if (root == NULL || !yyjson_is_obj(root) || array_name == NULL) {
5864 return 64;
5865 }
5866 mut_doc = yyjson_doc_mut_copy(doc, NULL);
5867 mut_root = yyjson_mut_doc_get_root(mut_doc);
5868 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
5870 yyjson_mut_doc_free(mut_doc);
5871 return 69;
5872 }
5873 errors = ensure_mut_array_field_runtime(mut_doc, mut_root, array_name);
5874 entry = yyjson_mut_obj(mut_doc);
5875 if (errors == NULL || entry == NULL) {
5877 yyjson_mut_doc_free(mut_doc);
5878 return 69;
5879 }
5880 if (field_id != NULL && field_id[0] != '\0' && !yyjson_mut_obj_add_strcpy(mut_doc, entry, "field_id", field_id)) {
5882 yyjson_mut_doc_free(mut_doc);
5883 return 69;
5884 }
5885 if (!yyjson_mut_obj_add_strcpy(mut_doc, entry, "rule_id", rule_id != NULL ? rule_id : "") || !yyjson_mut_obj_add_strcpy(mut_doc, entry, "code", code_text != NULL ? code_text : "") || !yyjson_mut_obj_add_strcpy(mut_doc, entry, "message", message != NULL ? message : "") || !yyjson_mut_arr_append(errors, entry)) {
5887 yyjson_mut_doc_free(mut_doc);
5888 return 69;
5889 }
5890 code = emit_compact_json_value_runtime((yyjson_val *)mut_root);
5892 yyjson_mut_doc_free(mut_doc);
5893 return code;
5894}
5895
5896static int handle_validation_add_field_error_command(int argc, char **argv) {
5897 return append_validation_error_runtime(json_option_value_runtime(argc, argv, "--report-json"), "field_errors", json_option_value_runtime(argc, argv, "--field-id"), json_option_value_runtime(argc, argv, "--rule-id"), json_option_value_runtime(argc, argv, "--code"), json_option_value_runtime(argc, argv, "--message"));
5898}
5899
5900static int handle_validation_add_form_error_command(int argc, char **argv) {
5901 return append_validation_error_runtime(json_option_value_runtime(argc, argv, "--report-json"), "form_errors", NULL, json_option_value_runtime(argc, argv, "--rule-id"), json_option_value_runtime(argc, argv, "--code"), json_option_value_runtime(argc, argv, "--message"));
5902}
5903
5904static int handle_validation_has_errors_command(int argc, char **argv) {
5905 const char *report_json = json_option_value_runtime(argc, argv, "--report-json");
5906 yyjson_doc *doc = NULL;
5907 yyjson_val *root = load_root_from_text_runtime(report_json, &doc);
5908 yyjson_val *field_errors = NULL;
5909 yyjson_val *form_errors = NULL;
5910 size_t count = 0;
5911 if (root != NULL && yyjson_is_obj(root)) {
5912 field_errors = native_json_obj_get_array(root, "field_errors");
5913 form_errors = native_json_obj_get_array(root, "form_errors");
5914 count = (field_errors != NULL ? yyjson_arr_size(field_errors) : 0) + (form_errors != NULL ? yyjson_arr_size(form_errors) : 0);
5915 }
5917 fputs(count > 0 ? "true" : "false", stdout);
5918 return count > 0 ? 0 : 1;
5919}
5920
5921static int handle_validation_summary_text_command(int argc, char **argv) {
5922 const char *report_json = json_option_value_runtime(argc, argv, "--report-json");
5923 yyjson_doc *doc = NULL;
5924 yyjson_val *root = load_root_from_text_runtime(report_json, &doc);
5925 yyjson_arr_iter iter;
5926 yyjson_val *items = NULL;
5927 yyjson_val *item = NULL;
5928 int first = 1;
5929 if (root == NULL || !yyjson_is_obj(root)) {
5931 return 64;
5932 }
5933 items = native_json_obj_get_array(root, "field_errors");
5934 if (items != NULL && yyjson_arr_iter_init(items, &iter)) {
5935 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
5936 char *rule_id = native_json_obj_get_string_dup(item, "rule_id");
5937 char *message = native_json_obj_get_string_dup(item, "message");
5938 if (message != NULL && message[0] != '\0') {
5939 if (!first) {
5940 fputc(' ', stdout);
5941 }
5942 fprintf(stdout, "[%s] %s", rule_id != NULL ? rule_id : "", message);
5943 first = 0;
5944 }
5945 free(rule_id);
5946 free(message);
5947 }
5948 }
5949 items = native_json_obj_get_array(root, "form_errors");
5950 if (items != NULL && yyjson_arr_iter_init(items, &iter)) {
5951 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
5952 char *rule_id = native_json_obj_get_string_dup(item, "rule_id");
5953 char *message = native_json_obj_get_string_dup(item, "message");
5954 if (message != NULL && message[0] != '\0') {
5955 if (!first) {
5956 fputc(' ', stdout);
5957 }
5958 fprintf(stdout, "[%s] %s", rule_id != NULL ? rule_id : "", message);
5959 first = 0;
5960 }
5961 free(rule_id);
5962 free(message);
5963 }
5964 }
5966 return 0;
5967}
5968
5969static int handle_validation_field_list_html_command(int argc, char **argv) {
5970 const char *report_json = json_option_value_runtime(argc, argv, "--report-json");
5971 yyjson_doc *doc = NULL;
5972 yyjson_val *root = load_root_from_text_runtime(report_json, &doc);
5973 yyjson_arr_iter iter;
5974 yyjson_val *items = NULL;
5975 yyjson_val *item = NULL;
5976 if (root == NULL || !yyjson_is_obj(root)) {
5978 return 64;
5979 }
5980 items = native_json_obj_get_array(root, "field_errors");
5981 if (items != NULL && yyjson_arr_iter_init(items, &iter)) {
5982 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
5983 char *field_id = native_json_obj_get_string_dup(item, "field_id");
5984 char *rule_id = native_json_obj_get_string_dup(item, "rule_id");
5985 char *message = native_json_obj_get_string_dup(item, "message");
5986 fprintf(stdout, "<li><code>%s</code> · <strong>%s</strong> · %s</li>", rule_id != NULL ? rule_id : "", field_id != NULL ? field_id : "", message != NULL ? message : "");
5987 free(field_id);
5988 free(rule_id);
5989 free(message);
5990 }
5991 }
5992 items = native_json_obj_get_array(root, "form_errors");
5993 if (items != NULL && yyjson_arr_iter_init(items, &iter)) {
5994 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
5995 char *rule_id = native_json_obj_get_string_dup(item, "rule_id");
5996 char *message = native_json_obj_get_string_dup(item, "message");
5997 fprintf(stdout, "<li><code>%s</code> · %s</li>", rule_id != NULL ? rule_id : "", message != NULL ? message : "");
5998 free(rule_id);
5999 free(message);
6000 }
6001 }
6003 return 0;
6004}
6005
6006static int handle_error_envelope_command(int argc, char **argv) {
6007 const char *error_type = json_option_value_runtime(argc, argv, "--error-type");
6008 const char *error_code = json_option_value_runtime(argc, argv, "--error-code");
6009 const char *message = json_option_value_runtime(argc, argv, "--message");
6010 const char *actor_scope = json_option_value_runtime(argc, argv, "--actor-scope");
6011 const char *tenant_scope = json_option_value_runtime(argc, argv, "--tenant-scope");
6012 const char *details_json = json_option_value_runtime(argc, argv, "--details-json");
6013 fprintf(stdout, "{\"schema\":\"pharos.error.v1\",\"ok\":false,\"error\":{\"type\":");
6014 emit_json_string_runtime(error_type != NULL ? error_type : "");
6015 fprintf(stdout, ",\"code\":");
6016 emit_json_string_runtime(error_code != NULL ? error_code : "");
6017 fprintf(stdout, ",\"message\":");
6018 emit_json_string_runtime(message != NULL ? message : "");
6019 fprintf(stdout, ",\"actor_scope\":");
6020 emit_json_string_runtime(actor_scope != NULL ? actor_scope : "anonymous");
6021 fprintf(stdout, ",\"tenant_scope\":");
6022 emit_json_string_runtime(tenant_scope != NULL ? tenant_scope : "global");
6023 fprintf(stdout, ",\"details\":%s}}", (details_json != NULL && details_json[0] != '\0') ? details_json : "null");
6024 return 0;
6025}
6026
6027static int handle_validation_error_envelope_command(int argc, char **argv) {
6028 const char *report_json = json_option_value_runtime(argc, argv, "--report-json");
6029 yyjson_doc *doc = NULL;
6030 yyjson_val *root = load_root_from_text_runtime(report_json, &doc);
6031 char *actor_scope = NULL;
6032 char *tenant_scope = NULL;
6033 int code = 0;
6034 if (root == NULL || !yyjson_is_obj(root)) {
6036 return 64;
6037 }
6038 actor_scope = native_json_obj_get_string_dup(root, "actor_scope");
6039 tenant_scope = native_json_obj_get_string_dup(root, "tenant_scope");
6041 fprintf(stdout, "{\"schema\":\"pharos.error.v1\",\"ok\":false,\"error\":{\"type\":\"validation_error\",\"code\":\"validation_failed\",\"message\":\"Validation failed.\",\"actor_scope\":");
6042 emit_json_string_runtime(actor_scope != NULL ? actor_scope : "anonymous");
6043 fprintf(stdout, ",\"tenant_scope\":");
6044 emit_json_string_runtime(tenant_scope != NULL ? tenant_scope : "global");
6045 fprintf(stdout, ",\"details\":%s}}", report_json != NULL ? report_json : "null");
6046 free(actor_scope);
6047 free(tenant_scope);
6048 return code;
6049}
6050
6051static int long_from_value_runtime(yyjson_val *value, long *out) {
6052 int64_t signed_value = 0;
6053 uint64_t unsigned_value = 0;
6054 double real_value = 0.0;
6055 if (value == NULL || out == NULL || !yyjson_is_num(value)) {
6056 return 0;
6057 }
6058 if (yyjson_is_sint(value)) {
6059 signed_value = yyjson_get_sint(value);
6060 *out = (long)signed_value;
6061 return 1;
6062 }
6063 if (yyjson_is_uint(value)) {
6064 unsigned_value = yyjson_get_uint(value);
6065 *out = (long)unsigned_value;
6066 return 1;
6067 }
6068 if (!yyjson_is_real(value)) {
6069 return 0;
6070 }
6071 real_value = yyjson_get_real(value);
6072 if ((double)((long)real_value) != real_value) {
6073 return 0;
6074 }
6075 *out = (long)real_value;
6076 return 1;
6077}
6078
6079static yyjson_val *find_array_item_by_string_field_runtime(yyjson_val *array_value, const char *field_name, const char *expected_value) {
6080 yyjson_arr_iter iter;
6081 yyjson_val *item = NULL;
6082 yyjson_val *field_value = NULL;
6083 if (array_value == NULL || field_name == NULL || expected_value == NULL || !yyjson_is_arr(array_value)) {
6084 return NULL;
6085 }
6086 if (!yyjson_arr_iter_init(array_value, &iter)) {
6087 return NULL;
6088 }
6089 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
6090 if (!yyjson_is_obj(item)) {
6091 continue;
6092 }
6093 field_value = native_json_obj_get(item, field_name);
6094 if (yyjson_is_str(field_value) && yyjson_equals_str(field_value, expected_value)) {
6095 return item;
6096 }
6097 }
6098 return NULL;
6099}
6100
6101static int value_long_equals_runtime(yyjson_val *value, long expected_value) {
6102 long actual_value = 0;
6103 return long_from_value_runtime(value, &actual_value) && actual_value == expected_value;
6104}
6105
6106static long max_id_in_state_section_runtime(yyjson_val *root, const char *section_name) {
6107 yyjson_val *array_value = native_json_obj_get_array(root, section_name);
6108 yyjson_arr_iter iter;
6109 yyjson_val *item = NULL;
6110 long max_id = 0;
6111 if (array_value == NULL || !yyjson_arr_iter_init(array_value, &iter)) {
6112 return 0;
6113 }
6114 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
6115 long item_id = 0;
6116 if (!yyjson_is_obj(item)) {
6117 continue;
6118 }
6119 if (long_from_value_runtime(native_json_obj_get(item, "id"), &item_id) && item_id > max_id) {
6120 max_id = item_id;
6121 }
6122 }
6123 return max_id;
6124}
6125
6127 yyjson_mut_val *array_value = NULL;
6128 if (doc == NULL || obj == NULL || field_name == NULL || field_name[0] == '\0') {
6129 return NULL;
6130 }
6131 array_value = yyjson_mut_obj_get(obj, field_name);
6132 if (array_value != NULL && yyjson_mut_is_arr(array_value)) {
6133 return array_value;
6134 }
6135 if (array_value != NULL) {
6136 yyjson_mut_obj_remove_key(obj, field_name);
6137 }
6138 array_value = yyjson_mut_arr(doc);
6139 if (array_value == NULL) {
6140 return NULL;
6141 }
6142 if (!yyjson_mut_obj_put(obj, yyjson_mut_strcpy(doc, field_name), array_value)) {
6143 return NULL;
6144 }
6145 return array_value;
6146}
6147
6149 yyjson_doc *payload_doc = NULL;
6150 yyjson_val *payload_root = NULL;
6151 yyjson_mut_val *payload_obj = NULL;
6152 payload_root = load_root_from_text_runtime(payload_json, &payload_doc);
6153 if (payload_root != NULL && yyjson_is_obj(payload_root)) {
6154 payload_obj = yyjson_val_mut_copy(doc, payload_root);
6155 }
6156 native_json_doc_free(payload_doc);
6157 if (payload_obj != NULL && yyjson_mut_is_obj(payload_obj)) {
6158 return payload_obj;
6159 }
6160 return yyjson_mut_obj(doc);
6161}
6162
6164 char *text = NULL;
6165 int code = 69;
6166 if (value == NULL) {
6167 return 69;
6168 }
6169 text = yyjson_mut_val_write(value, YYJSON_WRITE_NOFLAG, NULL);
6170 if (text != NULL) {
6171 fputs(text, stdout);
6172 code = 0;
6173 }
6174 free(text);
6175 return code;
6176}
6177
6178static yyjson_val *find_array_item_by_long_field_runtime(yyjson_val *array_value, const char *field_name, long expected_value) {
6179 yyjson_arr_iter iter;
6180 yyjson_val *item = NULL;
6181 yyjson_val *field_value = NULL;
6182 if (array_value == NULL || field_name == NULL || !yyjson_is_arr(array_value)) {
6183 return NULL;
6184 }
6185 if (!yyjson_arr_iter_init(array_value, &iter)) {
6186 return NULL;
6187 }
6188 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
6189 if (!yyjson_is_obj(item)) {
6190 continue;
6191 }
6192 field_value = native_json_obj_get(item, field_name);
6193 if (value_long_equals_runtime(field_value, expected_value)) {
6194 return item;
6195 }
6196 }
6197 return NULL;
6198}
6199
6200static yyjson_val *state_array_runtime(yyjson_val *root, const char *key) {
6201 return native_json_obj_get_array(root, key);
6202}
6203
6205 return find_array_item_by_string_field_runtime(state_array_runtime(root, "businesses"), "slug", slug);
6206}
6207
6209 return find_array_item_by_long_field_runtime(state_array_runtime(root, "businesses"), "id", business_id);
6210}
6211
6213 return find_array_item_by_long_field_runtime(state_array_runtime(root, "services"), "id", service_id);
6214}
6215
6216static yyjson_val *state_find_service_for_business_runtime(yyjson_val *root, long service_id, long business_id) {
6217 yyjson_arr_iter iter;
6218 yyjson_val *services = state_array_runtime(root, "services");
6219 yyjson_val *item = NULL;
6220 if (services == NULL || !yyjson_arr_iter_init(services, &iter)) {
6221 return NULL;
6222 }
6223 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
6224 if (!yyjson_is_obj(item)) {
6225 continue;
6226 }
6227 if (value_long_equals_runtime(native_json_obj_get(item, "id"), service_id) && value_long_equals_runtime(native_json_obj_get(item, "business_id"), business_id)) {
6228 return item;
6229 }
6230 }
6231 return NULL;
6232}
6233
6235 yyjson_arr_iter iter;
6236 yyjson_val *services = state_array_runtime(root, "services");
6237 yyjson_val *item = NULL;
6238 if (services == NULL || !yyjson_arr_iter_init(services, &iter)) {
6239 return NULL;
6240 }
6241 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
6242 if (yyjson_is_obj(item) && value_long_equals_runtime(native_json_obj_get(item, "business_id"), business_id)) {
6243 return item;
6244 }
6245 }
6246 return NULL;
6247}
6248
6249static int streq_casefold_runtime(const char *left, const char *right) {
6250 unsigned char left_ch = 0;
6251 unsigned char right_ch = 0;
6252 if (left == NULL || right == NULL) {
6253 return 0;
6254 }
6255 while (*left != '\0' && *right != '\0') {
6256 left_ch = (unsigned char)*left;
6257 right_ch = (unsigned char)*right;
6258 if (tolower(left_ch) != tolower(right_ch)) {
6259 return 0;
6260 }
6261 left++;
6262 right++;
6263 }
6264 return *left == '\0' && *right == '\0';
6265}
6266
6267static yyjson_val *state_find_user_by_username_runtime(yyjson_val *root, const char *username) {
6268 return find_array_item_by_string_field_runtime(state_array_runtime(root, "users"), "username", username);
6269}
6270
6271static yyjson_val *state_find_user_by_email_runtime(yyjson_val *root, const char *email) {
6272 yyjson_arr_iter iter;
6273 yyjson_val *users = state_array_runtime(root, "users");
6274 yyjson_val *user = NULL;
6275 if (users == NULL || email == NULL || email[0] == '\0' || !yyjson_arr_iter_init(users, &iter)) {
6276 return NULL;
6277 }
6278 while ((user = yyjson_arr_iter_next(&iter)) != NULL) {
6279 yyjson_val *active_value = NULL;
6280 char *candidate_email = NULL;
6281 int match = 0;
6282 if (!yyjson_is_obj(user)) {
6283 continue;
6284 }
6285 active_value = native_json_obj_get(user, "is_active");
6286 if (yyjson_is_bool(active_value) && !yyjson_get_bool(active_value)) {
6287 continue;
6288 }
6289 candidate_email = native_json_obj_get_string_dup(user, "email");
6290 match = candidate_email != NULL && streq_casefold_runtime(candidate_email, email);
6291 free(candidate_email);
6292 if (match) {
6293 return user;
6294 }
6295 }
6296 return NULL;
6297}
6298
6300 return find_array_item_by_long_field_runtime(state_array_runtime(root, "users"), "id", user_id);
6301}
6302
6304 return find_array_item_by_long_field_runtime(state_array_runtime(root, "appointments"), "id", appointment_id);
6305}
6306
6308 return find_array_item_by_long_field_runtime(state_array_runtime(root, "technicians"), "id", technician_id);
6309}
6310
6311static yyjson_val *state_find_technician_for_business_runtime(yyjson_val *root, long technician_id, long business_id) {
6312 yyjson_arr_iter iter;
6313 yyjson_val *technicians = state_array_runtime(root, "technicians");
6314 yyjson_val *item = NULL;
6315 if (technicians == NULL || !yyjson_arr_iter_init(technicians, &iter)) {
6316 return NULL;
6317 }
6318 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
6319 if (!yyjson_is_obj(item)) {
6320 continue;
6321 }
6322 if (value_long_equals_runtime(native_json_obj_get(item, "id"), technician_id) && value_long_equals_runtime(native_json_obj_get(item, "business_id"), business_id)) {
6323 return item;
6324 }
6325 }
6326 return NULL;
6327}
6328
6329static yyjson_val *state_find_business_availability_for_business_runtime(yyjson_val *root, long availability_id, long business_id) {
6330 yyjson_arr_iter iter;
6331 yyjson_val *rows = state_array_runtime(root, "business_availability");
6332 yyjson_val *item = NULL;
6333 if (rows == NULL || !yyjson_arr_iter_init(rows, &iter)) {
6334 return NULL;
6335 }
6336 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
6337 if (!yyjson_is_obj(item)) {
6338 continue;
6339 }
6340 if (value_long_equals_runtime(native_json_obj_get(item, "id"), availability_id) && value_long_equals_runtime(native_json_obj_get(item, "business_id"), business_id)) {
6341 return item;
6342 }
6343 }
6344 return NULL;
6345}
6346
6347static yyjson_val *state_find_booking_for_user_runtime(yyjson_val *root, long booking_id, long user_id) {
6348 yyjson_arr_iter iter;
6349 yyjson_val *bookings = state_array_runtime(root, "bookings");
6350 yyjson_val *item = NULL;
6351 if (bookings == NULL || !yyjson_arr_iter_init(bookings, &iter)) {
6352 return NULL;
6353 }
6354 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
6355 if (!yyjson_is_obj(item)) {
6356 continue;
6357 }
6358 if (value_long_equals_runtime(native_json_obj_get(item, "id"), booking_id) && value_long_equals_runtime(native_json_obj_get(item, "user_id"), user_id)) {
6359 return item;
6360 }
6361 }
6362 return NULL;
6363}
6364
6366 yyjson_arr_iter iter;
6367 yyjson_val *rows = state_array_runtime(root, "google_calendar_connections");
6368 yyjson_val *item = NULL;
6369 if (rows == NULL || !yyjson_arr_iter_init(rows, &iter)) {
6370 return NULL;
6371 }
6372 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
6373 if (yyjson_is_obj(item) && value_long_equals_runtime(native_json_obj_get(item, "business_id"), business_id)) {
6374 return item;
6375 }
6376 }
6377 return NULL;
6378}
6379
6380static long state_tenant_id_for_business_id_runtime(yyjson_val *root, long business_id) {
6381 yyjson_arr_iter iter;
6382 yyjson_val *tenants = state_array_runtime(root, "tenants");
6383 yyjson_val *item = NULL;
6384 if (tenants != NULL && yyjson_arr_iter_init(tenants, &iter)) {
6385 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
6386 if (yyjson_is_obj(item) && value_long_equals_runtime(native_json_obj_get(item, "source_business_id"), business_id)) {
6387 return native_json_obj_get_long_default(item, "id", business_id, NULL);
6388 }
6389 }
6390 }
6391 return business_id;
6392}
6393
6394static long state_tenant_id_for_business_slug_runtime(yyjson_val *root, const char *slug) {
6395 yyjson_arr_iter iter;
6396 yyjson_val *business = state_find_business_by_slug_runtime(root, slug);
6397 yyjson_val *tenants = state_array_runtime(root, "tenants");
6398 yyjson_val *item = NULL;
6399 long business_id = 0;
6400 if (business == NULL) {
6401 return 0;
6402 }
6403 business_id = native_json_obj_get_long_default(business, "id", 0, NULL);
6404 if (tenants != NULL && yyjson_arr_iter_init(tenants, &iter)) {
6405 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
6406 yyjson_val *slug_value = NULL;
6407 if (!yyjson_is_obj(item)) {
6408 continue;
6409 }
6410 slug_value = native_json_obj_get(item, "slug");
6411 if (value_long_equals_runtime(native_json_obj_get(item, "source_business_id"), business_id)) {
6412 return native_json_obj_get_long_default(item, "id", business_id, NULL);
6413 }
6414 if (yyjson_is_str(slug_value) && yyjson_equals_str(slug_value, slug)) {
6415 return native_json_obj_get_long_default(item, "id", business_id, NULL);
6416 }
6417 }
6418 }
6419 return business_id;
6420}
6421
6422static long state_tenant_id_for_service_id_runtime(yyjson_val *root, long service_id) {
6423 yyjson_val *service = state_find_service_by_id_runtime(root, service_id);
6424 long business_id = 0;
6425 if (service == NULL) {
6426 return 0;
6427 }
6428 business_id = native_json_obj_get_long_default(service, "business_id", 0, NULL);
6429 return state_tenant_id_for_business_id_runtime(root, business_id);
6430}
6431
6432static long state_tenant_id_for_technician_id_runtime(yyjson_val *root, long technician_id) {
6433 yyjson_val *technician = state_find_technician_by_id_runtime(root, technician_id);
6434 long business_id = 0;
6435 if (technician == NULL) {
6436 return 0;
6437 }
6438 business_id = native_json_obj_get_long_default(technician, "business_id", 0, NULL);
6439 return state_tenant_id_for_business_id_runtime(root, business_id);
6440}
6441
6443 if (value == NULL) {
6444 return 0;
6445 }
6446 return emit_compact_json_value_runtime(value);
6447}
6448
6449static int emit_long_runtime(long value) {
6450 fprintf(stdout, "%ld", value);
6451 return 0;
6452}
6453
6454static yyjson_mut_val *find_mut_array_item_by_long_field_runtime(yyjson_mut_val *array_value, const char *field_name, long expected_value) {
6455 size_t idx = 0;
6456 size_t max = 0;
6457 yyjson_mut_val *item = NULL;
6458 if (array_value == NULL || field_name == NULL || !yyjson_mut_is_arr(array_value)) {
6459 return NULL;
6460 }
6461 yyjson_mut_arr_foreach(array_value, idx, max, item) {
6462 if (yyjson_mut_is_obj(item) && value_long_equals_runtime((yyjson_val *)yyjson_mut_obj_get(item, field_name), expected_value)) {
6463 return item;
6464 }
6465 }
6466 return NULL;
6467}
6468
6469static int merge_patch_into_mut_object_runtime(yyjson_mut_doc *mut_doc, yyjson_mut_val *target_obj, yyjson_val *patch_root) {
6470 yyjson_obj_iter iter;
6471 yyjson_val *key = NULL;
6472 yyjson_val *value = NULL;
6473 if (mut_doc == NULL || target_obj == NULL || patch_root == NULL || !yyjson_mut_is_obj(target_obj) || !yyjson_is_obj(patch_root)) {
6474 return 0;
6475 }
6476 if (yyjson_obj_iter_init(patch_root, &iter)) {
6477 while ((key = yyjson_obj_iter_next(&iter)) != NULL) {
6478 if (!yyjson_is_str(key)) {
6479 continue;
6480 }
6481 value = yyjson_obj_iter_get_val(key);
6482 if (!yyjson_mut_obj_put(target_obj, yyjson_mut_strcpy(mut_doc, yyjson_get_str(key)), yyjson_val_mut_copy(mut_doc, value))) {
6483 return 0;
6484 }
6485 }
6486 }
6487 return 1;
6488}
6489
6490static int emit_object_string_field_runtime(yyjson_val *obj, const char *field_name, const char *default_value) {
6491 char *copy = NULL;
6492 if (obj == NULL) {
6493 fputs(default_value != NULL ? default_value : "", stdout);
6494 return 0;
6495 }
6496 copy = native_json_obj_get_string_dup(obj, field_name);
6497 if (copy != NULL) {
6498 fputs(copy, stdout);
6499 free(copy);
6500 } else if (default_value != NULL) {
6501 fputs(default_value, stdout);
6502 }
6503 return 0;
6504}
6505
6506static int emit_service_assigned_technician_ids_lines_runtime(yyjson_val *root, yyjson_val *service, long business_id) {
6507 yyjson_val *technician_ids = NULL;
6508 yyjson_arr_iter iter;
6509 yyjson_val *item = NULL;
6510 int emitted = 0;
6511 if (service != NULL) {
6512 technician_ids = native_json_obj_get_array(service, "technician_ids");
6513 if (technician_ids != NULL && yyjson_arr_iter_init(technician_ids, &iter)) {
6514 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
6515 long technician_id = 0;
6516 if (!long_from_value_runtime(item, &technician_id)) {
6517 continue;
6518 }
6519 fprintf(stdout, "%ld\n", technician_id);
6520 emitted = 1;
6521 }
6522 }
6523 }
6524 if (!emitted) {
6525 yyjson_arr_iter tech_iter;
6526 yyjson_val *technicians = state_array_runtime(root, "technicians");
6527 yyjson_val *technician = NULL;
6528 if (technicians != NULL && yyjson_arr_iter_init(technicians, &tech_iter)) {
6529 while ((technician = yyjson_arr_iter_next(&tech_iter)) != NULL) {
6530 if (yyjson_is_obj(technician) && value_long_equals_runtime(native_json_obj_get(technician, "business_id"), business_id)) {
6531 fprintf(stdout, "%ld\n", native_json_obj_get_long_default(technician, "id", 0, NULL));
6532 }
6533 }
6534 }
6535 }
6536 return 0;
6537}
6538
6539static int emit_business_technician_ids_lines_runtime(yyjson_val *root, long business_id) {
6540 yyjson_arr_iter iter;
6541 yyjson_val *rows = state_array_runtime(root, "technicians");
6542 yyjson_val *row = NULL;
6543 if (rows == NULL || !yyjson_arr_iter_init(rows, &iter)) {
6544 return 0;
6545 }
6546 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
6547 if (yyjson_is_obj(row) && value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
6548 fprintf(stdout, "%ld\n", native_json_obj_get_long_default(row, "id", 0, NULL));
6549 }
6550 }
6551 return 0;
6552}
6553
6559
6565
6567 if (list == NULL) {
6568 return;
6569 }
6570 free(list->items);
6571 list->items = NULL;
6572 list->count = 0;
6573 list->capacity = 0;
6574}
6575
6576static int append_business_admin_service_runtime(business_admin_service_list_runtime *list, yyjson_val *service, long sort_order, long service_id) {
6577 business_admin_service_runtime *items = NULL;
6578 size_t next_capacity = 0;
6579 if (list == NULL || service == NULL) {
6580 return 0;
6581 }
6582 if (list->count == list->capacity) {
6583 next_capacity = list->capacity == 0 ? 8 : list->capacity * 2;
6584 items = (business_admin_service_runtime *)realloc(list->items, next_capacity * sizeof(business_admin_service_runtime));
6585 if (items == NULL) {
6586 return 0;
6587 }
6588 list->items = items;
6589 list->capacity = next_capacity;
6590 }
6591 list->items[list->count].service = service;
6592 list->items[list->count].sort_order = sort_order;
6593 list->items[list->count].service_id = service_id;
6594 list->count++;
6595 return 1;
6596}
6597
6598static int compare_business_admin_service_runtime(const void *left_ptr, const void *right_ptr) {
6599 const business_admin_service_runtime *left = (const business_admin_service_runtime *)left_ptr;
6600 const business_admin_service_runtime *right = (const business_admin_service_runtime *)right_ptr;
6601 if (left->sort_order < right->sort_order) {
6602 return -1;
6603 }
6604 if (left->sort_order > right->sort_order) {
6605 return 1;
6606 }
6607 if (left->service_id < right->service_id) {
6608 return -1;
6609 }
6610 if (left->service_id > right->service_id) {
6611 return 1;
6612 }
6613 return 0;
6614}
6615
6617 yyjson_arr_iter iter;
6618 yyjson_val *service = NULL;
6619 if (list == NULL) {
6620 return 0;
6621 }
6622 if (services == NULL || !yyjson_arr_iter_init(services, &iter)) {
6623 return 1;
6624 }
6625 while ((service = yyjson_arr_iter_next(&iter)) != NULL) {
6626 long service_id = 0;
6627 long sort_order = 0;
6628 if (!yyjson_is_obj(service) || !value_long_equals_runtime(native_json_obj_get(service, "business_id"), business_id)) {
6629 continue;
6630 }
6631 service_id = native_json_obj_get_long_default(service, "id", 0, NULL);
6632 sort_order = native_json_obj_get_long_default(service, "sort_order", service_id, NULL);
6633 if (sort_order <= 0) {
6634 sort_order = service_id;
6635 }
6636 if (!append_business_admin_service_runtime(list, service, sort_order, service_id)) {
6637 return 0;
6638 }
6639 }
6640 if (list->count > 1) {
6642 }
6643 return 1;
6644}
6645
6646static int emit_business_public_page_json_runtime(yyjson_val *root, const char *slug, long user_id) {
6647 yyjson_val *business = state_find_business_by_slug_runtime(root, slug);
6648 yyjson_val *subscriptions = state_array_runtime(root, "subscriptions");
6649 yyjson_val *ratings = state_array_runtime(root, "ratings");
6650 yyjson_val *services = state_array_runtime(root, "services");
6651 business_admin_service_list_runtime service_list = {0};
6652 yyjson_mut_doc *mut_doc = NULL;
6653 yyjson_mut_val *page_obj = NULL;
6654 yyjson_mut_val *business_obj = NULL;
6655 yyjson_mut_val *ratings_array = NULL;
6656 yyjson_mut_val *services_array = NULL;
6657 yyjson_arr_iter iter;
6658 yyjson_val *row = NULL;
6659 long business_id = 0;
6660 int subscribed = 0;
6661 int code = 69;
6662 if (business == NULL) {
6663 return 0;
6664 }
6665 business_id = native_json_obj_get_long_default(business, "id", 0, NULL);
6666 if (subscriptions != NULL && yyjson_arr_iter_init(subscriptions, &iter)) {
6667 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
6668 if (!yyjson_is_obj(row)) {
6669 continue;
6670 }
6671 if (value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)
6672 && value_long_equals_runtime(native_json_obj_get(row, "user_id"), user_id)) {
6673 subscribed = 1;
6674 break;
6675 }
6676 }
6677 }
6678 if (!collect_business_service_list_runtime(services, business_id, &service_list)) {
6680 return 69;
6681 }
6682 mut_doc = yyjson_mut_doc_new(NULL);
6683 page_obj = mut_doc != NULL ? yyjson_mut_obj(mut_doc) : NULL;
6684 business_obj = mut_doc != NULL ? yyjson_mut_obj(mut_doc) : NULL;
6685 ratings_array = mut_doc != NULL ? yyjson_mut_arr(mut_doc) : NULL;
6686 services_array = mut_doc != NULL ? yyjson_mut_arr(mut_doc) : NULL;
6687 if (mut_doc == NULL || page_obj == NULL || business_obj == NULL || ratings_array == NULL || services_array == NULL) {
6689 yyjson_mut_doc_free(mut_doc);
6690 return 69;
6691 }
6692 yyjson_mut_doc_set_root(mut_doc, page_obj);
6693 {
6694 char *name = native_json_obj_get_string_dup(business, "name");
6695 char *description = native_json_obj_get_string_dup(business, "description");
6696 char *business_slug = native_json_obj_get_string_dup(business, "slug");
6697 char *image_url = native_json_obj_get_string_dup(business, "image_url");
6698 if (business_obj == NULL
6699 || !yyjson_mut_obj_add_int(mut_doc, business_obj, "id", business_id)
6700 || !yyjson_mut_obj_add_strcpy(mut_doc, business_obj, "name", name != NULL ? name : "")
6701 || !yyjson_mut_obj_add_strcpy(mut_doc, business_obj, "description", description != NULL ? description : "")
6702 || !yyjson_mut_obj_add_strcpy(mut_doc, business_obj, "slug", business_slug != NULL ? business_slug : "")
6703 || !yyjson_mut_obj_add_strcpy(mut_doc, business_obj, "image_url", image_url != NULL ? image_url : "")
6704 || !yyjson_mut_obj_add_bool(mut_doc, page_obj, "subscribed", subscribed)
6705 || !yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(mut_doc, "business"), business_obj)
6706 || !yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(mut_doc, "ratings"), ratings_array)
6707 || !yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(mut_doc, "services"), services_array)) {
6708 free(name);
6709 free(description);
6710 free(business_slug);
6711 free(image_url);
6713 yyjson_mut_doc_free(mut_doc);
6714 return 69;
6715 }
6716 free(name);
6717 free(description);
6718 free(business_slug);
6719 free(image_url);
6720 }
6721 if (ratings != NULL && yyjson_arr_iter_init(ratings, &iter)) {
6722 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
6723 yyjson_mut_val *rating_obj = NULL;
6724 char *comment = NULL;
6725 long score = 0;
6726 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
6727 continue;
6728 }
6729 rating_obj = yyjson_mut_obj(mut_doc);
6730 score = native_json_obj_get_long_default(row, "score", 0, NULL);
6731 comment = native_json_obj_get_string_dup(row, "comment");
6732 if (rating_obj == NULL
6733 || !yyjson_mut_obj_add_int(mut_doc, rating_obj, "score", score)
6734 || !yyjson_mut_obj_add_strcpy(mut_doc, rating_obj, "comment", comment != NULL ? comment : "")
6735 || !yyjson_mut_arr_append(ratings_array, rating_obj)) {
6736 free(comment);
6738 yyjson_mut_doc_free(mut_doc);
6739 return 69;
6740 }
6741 free(comment);
6742 }
6743 }
6744 {
6745 size_t index = 0;
6746 for (index = 0; index < service_list.count; index++) {
6747 yyjson_val *service = service_list.items[index].service;
6748 yyjson_mut_val *service_obj = yyjson_mut_obj(mut_doc);
6749 long service_id = service_list.items[index].service_id;
6750 long duration_minutes = native_json_obj_get_long_default(service, "duration_minutes", 45, NULL);
6751 long sort_order = service_list.items[index].sort_order;
6752 char *name = native_json_obj_get_string_dup(service, "name");
6753 char *cost = native_json_obj_get_string_dup(service, "cost");
6754 char *description = native_json_obj_get_string_dup(service, "description");
6755 char *image_url = native_json_obj_get_string_dup(service, "image_url");
6756 if (image_url == NULL || image_url[0] == '\0') {
6757 free(image_url);
6758 image_url = native_json_obj_get_string_dup(service, "image");
6759 }
6760 if (duration_minutes <= 0) {
6761 duration_minutes = 45;
6762 }
6763 if (service_obj == NULL
6764 || !yyjson_mut_obj_add_int(mut_doc, service_obj, "id", service_id)
6765 || !yyjson_mut_obj_add_strcpy(mut_doc, service_obj, "name", name != NULL ? name : "")
6766 || !yyjson_mut_obj_add_int(mut_doc, service_obj, "duration_minutes", duration_minutes)
6767 || !yyjson_mut_obj_add_strcpy(mut_doc, service_obj, "cost", cost != NULL ? cost : "")
6768 || !yyjson_mut_obj_add_strcpy(mut_doc, service_obj, "description", description != NULL ? description : "")
6769 || !yyjson_mut_obj_add_strcpy(mut_doc, service_obj, "image_url", image_url != NULL ? image_url : "")
6770 || !yyjson_mut_obj_add_int(mut_doc, service_obj, "sort_order", sort_order)
6771 || !yyjson_mut_arr_append(services_array, service_obj)) {
6772 free(name);
6773 free(cost);
6774 free(description);
6775 free(image_url);
6777 yyjson_mut_doc_free(mut_doc);
6778 return 69;
6779 }
6780 free(name);
6781 free(cost);
6782 free(description);
6783 free(image_url);
6784 }
6785 }
6786 code = emit_compact_json_mut_value_runtime(page_obj);
6788 yyjson_mut_doc_free(mut_doc);
6789 return code;
6790}
6791
6793 yyjson_val *services = state_array_runtime(root, "services");
6794 yyjson_arr_iter iter;
6795 yyjson_val *service = NULL;
6797 yyjson_mut_doc *mut_doc = NULL;
6798 yyjson_mut_val *array = NULL;
6799 size_t index = 0;
6800 int code = 69;
6801 if (services == NULL || !yyjson_arr_iter_init(services, &iter)) {
6802 fputs("[]", stdout);
6803 return 0;
6804 }
6805 while ((service = yyjson_arr_iter_next(&iter)) != NULL) {
6806 long service_id = 0;
6807 long sort_order = 0;
6808 if (!yyjson_is_obj(service) || !value_long_equals_runtime(native_json_obj_get(service, "business_id"), business_id)) {
6809 continue;
6810 }
6811 service_id = native_json_obj_get_long_default(service, "id", 0, NULL);
6812 sort_order = native_json_obj_get_long_default(service, "sort_order", service_id, NULL);
6813 if (sort_order <= 0) {
6814 sort_order = service_id;
6815 }
6816 if (!append_business_admin_service_runtime(&list, service, sort_order, service_id)) {
6818 return 69;
6819 }
6820 }
6821 if (list.count > 1) {
6823 }
6824 mut_doc = yyjson_mut_doc_new(NULL);
6825 array = mut_doc != NULL ? yyjson_mut_arr(mut_doc) : NULL;
6826 if (mut_doc == NULL || array == NULL) {
6828 yyjson_mut_doc_free(mut_doc);
6829 return 69;
6830 }
6831 yyjson_mut_doc_set_root(mut_doc, array);
6832 for (index = 0; index < list.count; index++) {
6833 yyjson_val *row = list.items[index].service;
6834 yyjson_mut_val *obj = yyjson_mut_obj(mut_doc);
6835 yyjson_val *booking_options = native_json_obj_get_array(row, "booking_options");
6836 yyjson_arr_iter option_iter;
6837 yyjson_val *option = NULL;
6838 yyjson_arr_iter tech_iter;
6839 yyjson_val *technicians = state_array_runtime(root, "technicians");
6840 yyjson_val *technician = NULL;
6841 yyjson_val *technician_ids = native_json_obj_get_array(row, "technician_ids");
6842 char *service_name = native_json_obj_get_string_dup(row, "name");
6843 char *service_cost = native_json_obj_get_string_dup(row, "cost");
6844 char *service_description = native_json_obj_get_string_dup(row, "description");
6845 char *image_url_raw = native_json_obj_get_string_dup(row, "image_url");
6846 char *booking_options_summary = NULL;
6847 char *technician_names = NULL;
6848 long duration_minutes = native_json_obj_get_long_default(row, "duration_minutes", 45, NULL);
6849 long order_index = (long)index + 1;
6850 int first_booking_option = 1;
6851 int first_technician = 1;
6852 if (image_url_raw == NULL || image_url_raw[0] == '\0') {
6853 free(image_url_raw);
6854 image_url_raw = native_json_obj_get_string_dup(row, "image");
6855 }
6856 if (booking_options != NULL && yyjson_arr_iter_init(booking_options, &option_iter)) {
6857 while ((option = yyjson_arr_iter_next(&option_iter)) != NULL) {
6858 char *label = native_json_obj_get_string_dup(option, "label");
6859 char *cost = native_json_obj_get_string_dup(option, "cost");
6860 long option_duration = native_json_obj_get_long_default(option, "duration_minutes", 45, NULL);
6861 char fallback_label[64];
6862 size_t current_len = booking_options_summary != NULL ? strlen(booking_options_summary) : 0;
6863 const char *label_text = label;
6864 if (label_text == NULL || label_text[0] == '\0') {
6865 snprintf(fallback_label, sizeof(fallback_label), "%ld min", option_duration);
6866 label_text = fallback_label;
6867 }
6868 {
6869 const char *cost_text = cost != NULL ? cost : "";
6870 size_t needed = current_len + (first_booking_option ? 0 : 2) + strlen(label_text) + 2 + strlen(cost_text) + 1;
6871 char *next = (char *)realloc(booking_options_summary, needed);
6872 if (next == NULL) {
6873 free(label);
6874 free(cost);
6875 free(service_name);
6876 free(service_cost);
6877 free(service_description);
6878 free(image_url_raw);
6879 free(booking_options_summary);
6880 free(technician_names);
6882 yyjson_mut_doc_free(mut_doc);
6883 return 69;
6884 }
6885 booking_options_summary = next;
6886 if (first_booking_option) {
6887 booking_options_summary[0] = '\0';
6888 } else {
6889 strcat(booking_options_summary, ", ");
6890 }
6891 strcat(booking_options_summary, label_text);
6892 strcat(booking_options_summary, " $");
6893 strcat(booking_options_summary, cost_text);
6894 first_booking_option = 0;
6895 }
6896 free(label);
6897 free(cost);
6898 }
6899 }
6900 if (technicians != NULL && yyjson_arr_iter_init(technicians, &tech_iter)) {
6901 while ((technician = yyjson_arr_iter_next(&tech_iter)) != NULL) {
6902 char *technician_name = NULL;
6903 long technician_id = 0;
6904 int selected = 0;
6905 yyjson_arr_iter assigned_iter;
6906 yyjson_val *assigned_item = NULL;
6907 if (!yyjson_is_obj(technician) || !value_long_equals_runtime(native_json_obj_get(technician, "business_id"), business_id)) {
6908 continue;
6909 }
6910 technician_id = native_json_obj_get_long_default(technician, "id", 0, NULL);
6911 if (technician_ids != NULL && yyjson_arr_iter_init(technician_ids, &assigned_iter)) {
6912 while ((assigned_item = yyjson_arr_iter_next(&assigned_iter)) != NULL) {
6913 long assigned_id = 0;
6914 if (long_from_value_runtime(assigned_item, &assigned_id) && assigned_id == technician_id) {
6915 selected = 1;
6916 break;
6917 }
6918 }
6919 } else {
6920 selected = 1;
6921 }
6922 if (!selected) {
6923 continue;
6924 }
6925 technician_name = native_json_obj_get_string_dup(technician, "name");
6926 if (technician_name != NULL && technician_name[0] != '\0') {
6927 size_t current_len = technician_names != NULL ? strlen(technician_names) : 0;
6928 size_t needed = current_len + (first_technician ? 0 : 2) + strlen(technician_name) + 1;
6929 char *next = (char *)realloc(technician_names, needed);
6930 if (next == NULL) {
6931 free(technician_name);
6932 free(service_name);
6933 free(service_cost);
6934 free(service_description);
6935 free(image_url_raw);
6936 free(booking_options_summary);
6937 free(technician_names);
6939 yyjson_mut_doc_free(mut_doc);
6940 return 69;
6941 }
6942 technician_names = next;
6943 if (first_technician) {
6944 technician_names[0] = '\0';
6945 } else {
6946 strcat(technician_names, ", ");
6947 }
6948 strcat(technician_names, technician_name);
6949 first_technician = 0;
6950 }
6951 free(technician_name);
6952 }
6953 }
6954 if (duration_minutes <= 0) {
6955 duration_minutes = 45;
6956 }
6957 if (obj == NULL
6958 || !yyjson_mut_obj_add_int(mut_doc, obj, "id", list.items[index].service_id)
6959 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "name", service_name != NULL ? service_name : "")
6960 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "cost", service_cost != NULL ? service_cost : "")
6961 || !yyjson_mut_obj_add_int(mut_doc, obj, "duration_minutes", duration_minutes)
6962 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "description", service_description != NULL ? service_description : "")
6963 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "image_url_raw", image_url_raw != NULL ? image_url_raw : "")
6964 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "booking_options_summary", booking_options_summary != NULL ? booking_options_summary : "")
6965 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "technician_names", technician_names != NULL ? technician_names : "")
6966 || !yyjson_mut_obj_add_int(mut_doc, obj, "order_index", order_index)
6967 || !yyjson_mut_obj_add_bool(mut_doc, obj, "move_up_disabled", index == 0)
6968 || !yyjson_mut_obj_add_bool(mut_doc, obj, "move_down_disabled", (index + 1) >= list.count)
6969 || !yyjson_mut_arr_append(array, obj)) {
6970 free(service_name);
6971 free(service_cost);
6972 free(service_description);
6973 free(image_url_raw);
6974 free(booking_options_summary);
6975 free(technician_names);
6977 yyjson_mut_doc_free(mut_doc);
6978 return 69;
6979 }
6980 free(service_name);
6981 free(service_cost);
6982 free(service_description);
6983 free(image_url_raw);
6984 free(booking_options_summary);
6985 free(technician_names);
6986 }
6989 yyjson_mut_doc_free(mut_doc);
6990 return code;
6991}
6992
6994 yyjson_val *technicians = state_array_runtime(root, "technicians");
6995 yyjson_val *technician_availability = state_array_runtime(root, "technician_availability");
6996 yyjson_arr_iter tech_iter;
6997 yyjson_val *technician = NULL;
6998 yyjson_mut_doc *mut_doc = NULL;
6999 yyjson_mut_val *array = NULL;
7000 int code = 69;
7001 if (technicians == NULL || !yyjson_arr_iter_init(technicians, &tech_iter)) {
7002 fputs("[]", stdout);
7003 return 0;
7004 }
7005 mut_doc = yyjson_mut_doc_new(NULL);
7006 array = mut_doc != NULL ? yyjson_mut_arr(mut_doc) : NULL;
7007 if (mut_doc == NULL || array == NULL) {
7008 yyjson_mut_doc_free(mut_doc);
7009 return 69;
7010 }
7011 yyjson_mut_doc_set_root(mut_doc, array);
7012 while ((technician = yyjson_arr_iter_next(&tech_iter)) != NULL) {
7013 yyjson_mut_val *obj = NULL;
7014 yyjson_arr_iter availability_iter;
7015 yyjson_val *availability = NULL;
7016 long technician_id = 0;
7017 char *name = NULL;
7018 char *email = NULL;
7019 char *summary_html = NULL;
7020 int first_entry = 1;
7021 if (!yyjson_is_obj(technician) || !value_long_equals_runtime(native_json_obj_get(technician, "business_id"), business_id)) {
7022 continue;
7023 }
7024 technician_id = native_json_obj_get_long_default(technician, "id", 0, NULL);
7025 name = native_json_obj_get_string_dup(technician, "name");
7026 email = native_json_obj_get_string_dup(technician, "email");
7027 if (technician_availability != NULL && yyjson_arr_iter_init(technician_availability, &availability_iter)) {
7028 while ((availability = yyjson_arr_iter_next(&availability_iter)) != NULL) {
7029 char *day = NULL;
7030 char *start = NULL;
7031 char *end = NULL;
7032 char *entry = NULL;
7033 size_t current_len = summary_html != NULL ? strlen(summary_html) : 0;
7034 size_t needed = 0;
7035 if (!yyjson_is_obj(availability) || !value_long_equals_runtime(native_json_obj_get(availability, "technician_id"), technician_id)) {
7036 continue;
7037 }
7038 day = native_json_obj_get_string_dup(availability, "day_of_week");
7039 start = native_json_obj_get_string_dup(availability, "start_time");
7040 end = native_json_obj_get_string_dup(availability, "end_time");
7041 if (day == NULL) {
7042 day = dup_range("", 0);
7043 }
7044 if (start == NULL) {
7045 start = dup_range("", 0);
7046 }
7047 if (end == NULL) {
7048 end = dup_range("", 0);
7049 }
7050 needed = strlen(day != NULL ? day : "") + strlen(start != NULL ? start : "") + strlen(end != NULL ? end : "") + 4;
7051 entry = (char *)malloc(needed);
7052 if (entry == NULL) {
7053 free(day);
7054 free(start);
7055 free(end);
7056 free(name);
7057 free(email);
7058 free(summary_html);
7059 yyjson_mut_doc_free(mut_doc);
7060 return 69;
7061 }
7062 snprintf(entry, needed, "%s %s–%s", day != NULL ? day : "", start != NULL ? start : "", end != NULL ? end : "");
7063 {
7064 size_t total = current_len + (first_entry ? 0 : 4) + strlen(entry) + 1;
7065 char *next = (char *)realloc(summary_html, total);
7066 if (next == NULL) {
7067 free(entry);
7068 free(day);
7069 free(start);
7070 free(end);
7071 free(name);
7072 free(email);
7073 free(summary_html);
7074 yyjson_mut_doc_free(mut_doc);
7075 return 69;
7076 }
7077 summary_html = next;
7078 if (first_entry) {
7079 summary_html[0] = '\0';
7080 } else {
7081 strcat(summary_html, "<br>");
7082 }
7083 strcat(summary_html, entry);
7084 first_entry = 0;
7085 }
7086 free(entry);
7087 free(day);
7088 free(start);
7089 free(end);
7090 }
7091 }
7092 if (summary_html == NULL) {
7093 summary_html = dup_range("No technician availability set.", strlen("No technician availability set."));
7094 }
7095 obj = yyjson_mut_obj(mut_doc);
7096 if (obj == NULL
7097 || !yyjson_mut_obj_add_int(mut_doc, obj, "id", technician_id)
7098 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "name", name != NULL ? name : "")
7099 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "email", email != NULL ? email : "")
7100 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "availability_summary_html", summary_html != NULL ? summary_html : "")
7101 || !yyjson_mut_arr_append(array, obj)) {
7102 free(name);
7103 free(email);
7104 free(summary_html);
7105 yyjson_mut_doc_free(mut_doc);
7106 return 69;
7107 }
7108 free(name);
7109 free(email);
7110 free(summary_html);
7111 }
7113 yyjson_mut_doc_free(mut_doc);
7114 return code;
7115}
7116
7117static int emit_business_calendar_page_json_runtime(yyjson_val *root, const char *slug) {
7118 yyjson_val *business = state_find_business_by_slug_runtime(root, slug);
7119 yyjson_val *availability_rows = state_array_runtime(root, "business_availability");
7120 yyjson_val *appointments = state_array_runtime(root, "appointments");
7121 yyjson_mut_doc *mut_doc = NULL;
7122 yyjson_mut_val *page_obj = NULL;
7123 yyjson_mut_val *business_obj = NULL;
7124 yyjson_mut_val *availability_array = NULL;
7125 yyjson_mut_val *appointments_array = NULL;
7126 yyjson_arr_iter iter;
7127 yyjson_val *row = NULL;
7128 long business_id = 0;
7129 long first_service_id = 0;
7130 int code = 69;
7131 if (business == NULL) {
7132 return 0;
7133 }
7134 business_id = native_json_obj_get_long_default(business, "id", 0, NULL);
7135 {
7136 yyjson_val *first_service = state_find_first_service_for_business_runtime(root, business_id);
7137 if (first_service != NULL) {
7138 first_service_id = native_json_obj_get_long_default(first_service, "id", 0, NULL);
7139 }
7140 }
7141 mut_doc = yyjson_mut_doc_new(NULL);
7142 page_obj = mut_doc != NULL ? yyjson_mut_obj(mut_doc) : NULL;
7143 business_obj = mut_doc != NULL ? yyjson_mut_obj(mut_doc) : NULL;
7144 availability_array = mut_doc != NULL ? yyjson_mut_arr(mut_doc) : NULL;
7145 appointments_array = mut_doc != NULL ? yyjson_mut_arr(mut_doc) : NULL;
7146 if (mut_doc == NULL || page_obj == NULL || business_obj == NULL || availability_array == NULL || appointments_array == NULL) {
7147 yyjson_mut_doc_free(mut_doc);
7148 return 69;
7149 }
7150 yyjson_mut_doc_set_root(mut_doc, page_obj);
7151 {
7152 char *name = native_json_obj_get_string_dup(business, "name");
7153 char *business_slug = native_json_obj_get_string_dup(business, "slug");
7154 if (business_obj == NULL
7155 || !yyjson_mut_obj_add_int(mut_doc, business_obj, "id", business_id)
7156 || !yyjson_mut_obj_add_strcpy(mut_doc, business_obj, "name", name != NULL ? name : "")
7157 || !yyjson_mut_obj_add_strcpy(mut_doc, business_obj, "slug", business_slug != NULL ? business_slug : "")
7158 || !yyjson_mut_obj_add_int(mut_doc, page_obj, "first_service_id", first_service_id)
7159 || !yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(mut_doc, "business"), business_obj)
7160 || !yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(mut_doc, "availability"), availability_array)
7161 || !yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(mut_doc, "appointments"), appointments_array)) {
7162 free(name);
7163 free(business_slug);
7164 yyjson_mut_doc_free(mut_doc);
7165 return 69;
7166 }
7167 free(name);
7168 free(business_slug);
7169 }
7170 if (availability_rows != NULL && yyjson_arr_iter_init(availability_rows, &iter)) {
7171 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
7172 yyjson_mut_val *availability_obj = NULL;
7173 char *day_of_week = NULL;
7174 char *start_time = NULL;
7175 char *end_time = NULL;
7176 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
7177 continue;
7178 }
7179 availability_obj = yyjson_mut_obj(mut_doc);
7180 day_of_week = native_json_obj_get_string_dup(row, "day_of_week");
7181 start_time = native_json_obj_get_string_dup(row, "start_time");
7182 end_time = native_json_obj_get_string_dup(row, "end_time");
7183 if (availability_obj == NULL
7184 || !yyjson_mut_obj_add_strcpy(mut_doc, availability_obj, "day_of_week", day_of_week != NULL ? day_of_week : "")
7185 || !yyjson_mut_obj_add_strcpy(mut_doc, availability_obj, "start_time", start_time != NULL ? start_time : "")
7186 || !yyjson_mut_obj_add_strcpy(mut_doc, availability_obj, "end_time", end_time != NULL ? end_time : "")
7187 || !yyjson_mut_arr_append(availability_array, availability_obj)) {
7188 free(day_of_week);
7189 free(start_time);
7190 free(end_time);
7191 yyjson_mut_doc_free(mut_doc);
7192 return 69;
7193 }
7194 free(day_of_week);
7195 free(start_time);
7196 free(end_time);
7197 }
7198 }
7199 if (appointments != NULL && yyjson_arr_iter_init(appointments, &iter)) {
7200 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
7201 yyjson_mut_val *appointment_obj = NULL;
7202 yyjson_val *service = NULL;
7203 yyjson_val *technician = NULL;
7204 long service_id = 0;
7205 long technician_id = 0;
7206 char *date = NULL;
7207 char *time = NULL;
7208 char *status = NULL;
7209 char *service_name = NULL;
7210 char *technician_name = NULL;
7211 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
7212 continue;
7213 }
7214 service_id = native_json_obj_get_long_default(row, "service_id", 0, NULL);
7215 technician_id = native_json_obj_get_long_default(row, "technician_id", 0, NULL);
7216 service = state_find_service_by_id_runtime(root, service_id);
7217 technician = state_find_technician_for_business_runtime(root, technician_id, business_id);
7218 appointment_obj = yyjson_mut_obj(mut_doc);
7219 date = native_json_obj_get_string_dup(row, "date");
7220 time = native_json_obj_get_string_dup(row, "time");
7221 status = native_json_obj_get_string_dup(row, "status");
7222 service_name = service != NULL ? native_json_obj_get_string_dup(service, "name") : NULL;
7223 technician_name = technician != NULL ? native_json_obj_get_string_dup(technician, "name") : dup_range("Unassigned", strlen("Unassigned"));
7224 if (appointment_obj == NULL
7225 || !yyjson_mut_obj_add_int(mut_doc, appointment_obj, "id", native_json_obj_get_long_default(row, "id", 0, NULL))
7226 || !yyjson_mut_obj_add_strcpy(mut_doc, appointment_obj, "date", date != NULL ? date : "")
7227 || !yyjson_mut_obj_add_strcpy(mut_doc, appointment_obj, "time", time != NULL ? time : "")
7228 || !yyjson_mut_obj_add_strcpy(mut_doc, appointment_obj, "status", status != NULL ? status : "")
7229 || !yyjson_mut_obj_add_strcpy(mut_doc, appointment_obj, "service_name", service_name != NULL ? service_name : "")
7230 || !yyjson_mut_obj_add_strcpy(mut_doc, appointment_obj, "technician_name", technician_name != NULL ? technician_name : "Unassigned")
7231 || !yyjson_mut_arr_append(appointments_array, appointment_obj)) {
7232 free(date);
7233 free(time);
7234 free(status);
7235 free(service_name);
7236 free(technician_name);
7237 yyjson_mut_doc_free(mut_doc);
7238 return 69;
7239 }
7240 free(date);
7241 free(time);
7242 free(status);
7243 free(service_name);
7244 free(technician_name);
7245 }
7246 }
7247 code = emit_compact_json_mut_value_runtime(page_obj);
7248 yyjson_mut_doc_free(mut_doc);
7249 return code;
7250}
7251
7252static int emit_business_public_render_page_json_runtime(yyjson_val *root, const char *slug, long user_id, const char *app_root) {
7253 yyjson_val *business = state_find_business_by_slug_runtime(root, slug);
7254 yyjson_val *subscriptions = state_array_runtime(root, "subscriptions");
7255 yyjson_val *ratings = state_array_runtime(root, "ratings");
7256 yyjson_val *services = state_array_runtime(root, "services");
7257 business_admin_service_list_runtime service_list = {0};
7258 yyjson_mut_doc *page_doc = NULL;
7259 yyjson_mut_val *page_obj = NULL;
7260 yyjson_arr_iter iter;
7261 yyjson_val *row = NULL;
7262 TextBufferRuntime rating_rows = {0};
7263 TextBufferRuntime service_cards = {0};
7264 char *business_name = NULL;
7265 char *business_description = NULL;
7266 char *raw_business_image = NULL;
7267 char *business_image_url = NULL;
7268 char *business_image_html = NULL;
7269 char *rating_form_html = NULL;
7270 char *cta_html = NULL;
7271 char *empty_html = NULL;
7272 long business_id = 0;
7273 int subscribed = 0;
7274 int code = 69;
7275 if (business == NULL || app_root == NULL || app_root[0] == '\0') {
7276 return 64;
7277 }
7278 business_id = native_json_obj_get_long_default(business, "id", 0, NULL);
7279 business_name = native_json_obj_get_string_dup(business, "name");
7280 business_description = native_json_obj_get_string_dup(business, "description");
7281 raw_business_image = native_json_obj_get_string_dup(business, "image_url");
7282 business_image_url = business_media_url_dup_runtime(slug, raw_business_image != NULL ? raw_business_image : "");
7283 if (subscriptions != NULL && yyjson_arr_iter_init(subscriptions, &iter)) {
7284 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
7285 if (yyjson_is_obj(row)
7286 && value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)
7287 && value_long_equals_runtime(native_json_obj_get(row, "user_id"), user_id)) {
7288 subscribed = 1;
7289 break;
7290 }
7291 }
7292 }
7293 if (business_image_url != NULL && business_image_url[0] != '\0') {
7295 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
7296 if (doc == NULL || params == NULL) {
7298 goto business_public_cleanup;
7299 }
7300 yyjson_mut_doc_set_root(doc, params);
7301 yyjson_mut_obj_add_strcpy(doc, params, "image_url", business_image_url);
7302 yyjson_mut_obj_add_strcpy(doc, params, "image_alt", business_name != NULL ? business_name : "");
7303 business_image_html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/business_landing_image.html", doc);
7305 if (business_image_html == NULL) {
7306 goto business_public_cleanup;
7307 }
7308 }
7309 if (ratings != NULL && yyjson_arr_iter_init(ratings, &iter)) {
7310 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
7311 yyjson_mut_doc *doc = NULL;
7312 yyjson_mut_val *params = NULL;
7313 char *comment = NULL;
7314 char *html = NULL;
7315 long score = 0;
7316 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
7317 continue;
7318 }
7319 score = native_json_obj_get_long_default(row, "score", 0, NULL);
7320 comment = native_json_obj_get_string_dup(row, "comment");
7321 doc = yyjson_mut_doc_new(NULL);
7322 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
7323 if (doc == NULL || params == NULL) {
7324 free(comment);
7326 goto business_public_cleanup;
7327 }
7328 yyjson_mut_doc_set_root(doc, params);
7329 yyjson_mut_obj_add_int(doc, params, "rating_score", score);
7330 yyjson_mut_obj_add_strcpy(doc, params, "rating_comment", comment != NULL ? comment : "");
7331 html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/business_rating_row.html", doc);
7333 free(comment);
7334 if (html == NULL || !text_buffer_append_text_runtime(&rating_rows, html)) {
7335 free(html);
7336 goto business_public_cleanup;
7337 }
7338 free(html);
7339 }
7340 }
7341 if (rating_rows.length == 0) {
7343 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
7344 if (doc == NULL || params == NULL) {
7346 goto business_public_cleanup;
7347 }
7348 yyjson_mut_doc_set_root(doc, params);
7349 empty_html = render_business_fragment_dup_runtime(app_root, "templates/businesses/fragments/business_rating_empty.html", (yyjson_val *)params);
7351 if (empty_html == NULL || !text_buffer_append_text_runtime(&rating_rows, empty_html)) {
7352 free(empty_html);
7353 goto business_public_cleanup;
7354 }
7355 free(empty_html);
7356 empty_html = NULL;
7357 }
7358 if (!collect_business_service_list_runtime(services, business_id, &service_list)) {
7359 goto business_public_cleanup;
7360 }
7361 if (service_list.count > 0) {
7362 size_t index = 0;
7363 for (index = 0; index < service_list.count; index++) {
7364 yyjson_val *service = service_list.items[index].service;
7366 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
7367 char *service_name = native_json_obj_get_string_dup(service, "name");
7368 char *service_cost = native_json_obj_get_string_dup(service, "cost");
7369 char *service_description = native_json_obj_get_string_dup(service, "description");
7370 char *image_url_raw = native_json_obj_get_string_dup(service, "image_url");
7371 char *service_image_url = NULL;
7372 char *visual_html = NULL;
7373 char *card_html = NULL;
7374 long service_id = service_list.items[index].service_id;
7375 long duration_minutes = native_json_obj_get_long_default(service, "duration_minutes", 45, NULL);
7376 if (doc == NULL || params == NULL) {
7378 free(service_name); free(service_cost); free(service_description); free(image_url_raw);
7379 goto business_public_cleanup;
7380 }
7381 if (image_url_raw == NULL || image_url_raw[0] == '\0') {
7382 free(image_url_raw);
7383 image_url_raw = native_json_obj_get_string_dup(service, "image");
7384 }
7385 service_image_url = service_media_url_dup_runtime(slug, image_url_raw != NULL ? image_url_raw : "");
7386 visual_html = render_service_visual_html_dup_runtime(app_root, service_image_url != NULL ? service_image_url : "", service_name != NULL ? service_name : "");
7387 if (duration_minutes <= 0) {
7388 duration_minutes = 45;
7389 }
7390 yyjson_mut_doc_set_root(doc, params);
7391 yyjson_mut_obj_add_strcpy(doc, params, "business_slug", slug != NULL ? slug : "");
7392 yyjson_mut_obj_add_int(doc, params, "service_id", service_id);
7393 yyjson_mut_obj_add_strcpy(doc, params, "service_visual_html", visual_html != NULL ? visual_html : "");
7394 yyjson_mut_obj_add_strcpy(doc, params, "service_name", service_name != NULL ? service_name : "");
7395 yyjson_mut_obj_add_int(doc, params, "service_duration_minutes", duration_minutes);
7396 yyjson_mut_obj_add_strcpy(doc, params, "service_cost", service_cost != NULL ? service_cost : "");
7397 yyjson_mut_obj_add_strcpy(doc, params, "service_description", service_description != NULL ? service_description : "");
7398 card_html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/business_service_card.html", doc);
7400 free(service_name); free(service_cost); free(service_description); free(image_url_raw); free(service_image_url); free(visual_html);
7401 if (card_html == NULL || !text_buffer_append_text_runtime(&service_cards, card_html)) {
7402 free(card_html);
7403 goto business_public_cleanup;
7404 }
7405 free(card_html);
7406 }
7407 }
7408 if (service_cards.length == 0) {
7410 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
7411 if (doc == NULL || params == NULL) {
7413 goto business_public_cleanup;
7414 }
7415 yyjson_mut_doc_set_root(doc, params);
7416 empty_html = render_business_fragment_dup_runtime(app_root, "templates/businesses/fragments/business_services_empty.html", (yyjson_val *)params);
7418 if (empty_html == NULL || !text_buffer_append_text_runtime(&service_cards, empty_html)) {
7419 free(empty_html);
7420 goto business_public_cleanup;
7421 }
7422 free(empty_html);
7423 empty_html = NULL;
7424 }
7425 if (user_id > 0) {
7427 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
7428 if (doc == NULL || params == NULL) {
7430 goto business_public_cleanup;
7431 }
7432 yyjson_mut_doc_set_root(doc, params);
7433 yyjson_mut_obj_add_strcpy(doc, params, "business_slug", slug != NULL ? slug : "");
7434 rating_form_html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/business_rating_form.html", doc);
7436 if (rating_form_html == NULL) {
7437 goto business_public_cleanup;
7438 }
7439 }
7440 {
7442 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
7443 const char *fragment_path = NULL;
7444 if (doc == NULL || params == NULL) {
7446 goto business_public_cleanup;
7447 }
7448 yyjson_mut_doc_set_root(doc, params);
7449 yyjson_mut_obj_add_strcpy(doc, params, "business_slug", slug != NULL ? slug : "");
7450 yyjson_mut_obj_add_strcpy(doc, params, "business_name", business_name != NULL ? business_name : "");
7451 if (user_id <= 0) {
7452 fragment_path = "templates/businesses/fragments/business_logged_out_cta.html";
7453 } else if (subscribed) {
7454 fragment_path = "templates/businesses/fragments/business_subscribed_cta.html";
7455 } else {
7456 fragment_path = "templates/businesses/fragments/business_subscribe_cta.html";
7457 }
7458 cta_html = render_business_fragment_from_doc_dup_runtime(app_root, fragment_path, doc);
7460 if (cta_html == NULL) {
7461 goto business_public_cleanup;
7462 }
7463 }
7464 page_doc = yyjson_mut_doc_new(NULL);
7465 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
7466 if (page_doc == NULL || page_obj == NULL) {
7467 goto business_public_cleanup;
7468 }
7469 yyjson_mut_doc_set_root(page_doc, page_obj);
7470 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "business_name", business_name != NULL ? business_name : "");
7471 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "business_image_html", business_image_html != NULL ? business_image_html : "");
7472 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "business_description", business_description != NULL ? business_description : "");
7473 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "cta_html", cta_html != NULL ? cta_html : "");
7474 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "service_cards_html", service_cards.text != NULL ? service_cards.text : "");
7475 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "rating_rows_html", rating_rows.text != NULL ? rating_rows.text : "");
7476 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "rating_form_html", rating_form_html != NULL ? rating_form_html : "");
7477 code = emit_compact_json_mut_value_runtime(page_obj);
7478
7479business_public_cleanup:
7481 text_buffer_free_runtime(&rating_rows);
7482 text_buffer_free_runtime(&service_cards);
7483 free(business_name);
7484 free(business_description);
7485 free(raw_business_image);
7486 free(business_image_url);
7487 free(business_image_html);
7488 free(rating_form_html);
7489 free(cta_html);
7490 yyjson_mut_doc_free(page_doc);
7491 return code;
7492}
7493
7494static int emit_business_calendar_render_page_json_runtime(yyjson_val *root, const char *slug, const char *app_root, const char *selected_year_text, const char *selected_month_text, const char *selected_date_text) {
7495 yyjson_val *business = state_find_business_by_slug_runtime(root, slug);
7496 yyjson_val *availability_rows = state_array_runtime(root, "business_availability");
7497 yyjson_val *appointments = state_array_runtime(root, "appointments");
7498 yyjson_mut_doc *page_doc = NULL;
7499 yyjson_mut_val *page_obj = NULL;
7500 yyjson_arr_iter iter;
7501 yyjson_val *row = NULL;
7502 TextBufferRuntime calendar_rows = {0};
7503 TextBufferRuntime week_row = {0};
7504 TextBufferRuntime selected_schedule = {0};
7505 TextBufferRuntime appointment_rows = {0};
7506 char *business_name = NULL;
7507 char *landing_path = NULL;
7508 char *manage_path = NULL;
7509 char *prev_month_path = NULL;
7510 char *next_month_path = NULL;
7511 char *selected_date = NULL;
7512 long business_id = 0;
7513 int year = 0;
7514 int month = 0;
7515 int prev_year = 0;
7516 int prev_month = 0;
7517 int next_year = 0;
7518 int next_month = 0;
7519 int month_days = 0;
7520 int first_offset = 0;
7521 int day_cell_count = 0;
7522 int day_number = 1;
7523 int code = 69;
7524 time_t now = time(NULL);
7525 struct tm *local = localtime(&now);
7526 if (business == NULL || app_root == NULL || app_root[0] == '\0') {
7527 return 64;
7528 }
7529 business_id = native_json_obj_get_long_default(business, "id", 0, NULL);
7530 business_name = native_json_obj_get_string_dup(business, "name");
7531 year = (selected_year_text != NULL && selected_year_text[0] != '\0') ? (int)strtol(selected_year_text, NULL, 10) : (local != NULL ? local->tm_year + 1900 : 1970);
7532 month = (selected_month_text != NULL && selected_month_text[0] != '\0') ? (int)strtol(selected_month_text, NULL, 10) : (local != NULL ? local->tm_mon + 1 : 1);
7533 if (year < 1970) year = 1970;
7534 if (month < 1) month = 1;
7535 if (month > 12) month = 12;
7536 if (selected_date_text != NULL && selected_date_text[0] != '\0') {
7537 selected_date = strdup(selected_date_text);
7538 } else {
7539 TextBufferRuntime date_buffer = {0};
7540 if (!append_date_runtime(&date_buffer, year, month, 1)) {
7541 text_buffer_free_runtime(&date_buffer);
7542 goto business_calendar_render_cleanup;
7543 }
7544 selected_date = text_buffer_take_runtime(&date_buffer);
7545 }
7546 month_shift_runtime(year, month, -1, &prev_year, &prev_month);
7547 month_shift_runtime(year, month, 1, &next_year, &next_month);
7548 month_days = days_in_month_runtime(year, month);
7549 first_offset = weekday_monday_index_runtime(year, month, 1) - 1;
7550 while (day_cell_count < first_offset) {
7551 if (!text_buffer_append_text_runtime(&week_row, "<td class=\"day\"></td>")) {
7552 goto business_calendar_render_cleanup;
7553 }
7554 day_cell_count++;
7555 }
7556 while (day_number <= month_days) {
7557 TextBufferRuntime date_value = {0};
7558 const char *day_name = NULL;
7559 int available_count = 0;
7560 int appointment_count = 0;
7561 char *date_text = NULL;
7562 int selected = 0;
7563 if (!append_date_runtime(&date_value, year, month, day_number)) {
7564 text_buffer_free_runtime(&date_value);
7565 goto business_calendar_render_cleanup;
7566 }
7567 date_text = text_buffer_take_runtime(&date_value);
7568 day_name = day_abbrev_for_ymd_runtime(year, month, day_number);
7569 if (availability_rows != NULL && yyjson_arr_iter_init(availability_rows, &iter)) {
7570 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
7571 char *row_day = NULL;
7572 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
7573 continue;
7574 }
7575 row_day = native_json_obj_get_string_dup(row, "day_of_week");
7576 if (row_day != NULL && streq(row_day, day_name)) {
7577 available_count++;
7578 }
7579 free(row_day);
7580 }
7581 }
7582 if (appointments != NULL && yyjson_arr_iter_init(appointments, &iter)) {
7583 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
7584 char *row_date = NULL;
7585 char *status = NULL;
7586 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
7587 continue;
7588 }
7589 row_date = native_json_obj_get_string_dup(row, "date");
7590 status = native_json_obj_get_string_dup(row, "status");
7591 if (row_date != NULL && streq(row_date, date_text) && !(status != NULL && streq(status, "cancelled"))) {
7592 appointment_count++;
7593 }
7594 free(row_date);
7595 free(status);
7596 }
7597 }
7598 selected = selected_date != NULL && streq(selected_date, date_text);
7599 if (!text_buffer_append_format_runtime(&week_row, "<td class=\"day%s%s\" style=\"vertical-align:top;padding:0.5rem;min-width:88px;\">", available_count == 0 ? " unavailable" : "", selected ? " today" : "") ||
7600 !text_buffer_append_format_runtime(&week_row, "<a href=\"/business/%s/calendar/?year=%04d&month=%02d&date=%s\" class=\"btn calendar-day-chip\" style=\"display:inline-block;min-width:2.25rem;text-align:center;%s\">%d</a>", slug != NULL ? slug : "", year, month, date_text, selected ? "background:var(--primary);color:#fff;" : "background:var(--gray-200);color:var(--gray-700);", day_number)) {
7601 free(date_text);
7602 goto business_calendar_render_cleanup;
7603 }
7604 if (appointment_count > 0 && !text_buffer_append_format_runtime(&week_row, "<div style=\"margin-top:0.4rem;font-size:0.75rem;color:var(--gray-700);\">%d booked</div>", appointment_count)) {
7605 free(date_text);
7606 goto business_calendar_render_cleanup;
7607 }
7608 if (available_count > 0) {
7609 if (!text_buffer_append_format_runtime(&week_row, "<div style=\"margin-top:0.2rem;font-size:0.75rem;color:var(--primary);\">%d slots</div>", available_count)) {
7610 free(date_text);
7611 goto business_calendar_render_cleanup;
7612 }
7613 } else if (!text_buffer_append_text_runtime(&week_row, "<div style=\"margin-top:0.2rem;font-size:0.75rem;color:var(--gray-500);\">No slots</div>")) {
7614 free(date_text);
7615 goto business_calendar_render_cleanup;
7616 }
7617 if (!text_buffer_append_text_runtime(&week_row, "</td>")) {
7618 free(date_text);
7619 goto business_calendar_render_cleanup;
7620 }
7621 free(date_text);
7622 day_cell_count++;
7623 if ((day_cell_count % 7) == 0) {
7624 if (!text_buffer_append_text_runtime(&calendar_rows, "<tr>") || !text_buffer_append_text_runtime(&calendar_rows, week_row.text != NULL ? week_row.text : "") || !text_buffer_append_text_runtime(&calendar_rows, "</tr>")) {
7625 goto business_calendar_render_cleanup;
7626 }
7627 text_buffer_free_runtime(&week_row);
7628 }
7629 day_number++;
7630 }
7631 while ((day_cell_count % 7) != 0) {
7632 if (!text_buffer_append_text_runtime(&week_row, "<td class=\"day\"></td>")) {
7633 goto business_calendar_render_cleanup;
7634 }
7635 day_cell_count++;
7636 }
7637 if (week_row.length > 0) {
7638 if (!text_buffer_append_text_runtime(&calendar_rows, "<tr>") || !text_buffer_append_text_runtime(&calendar_rows, week_row.text != NULL ? week_row.text : "") || !text_buffer_append_text_runtime(&calendar_rows, "</tr>")) {
7639 goto business_calendar_render_cleanup;
7640 }
7641 }
7642 {
7643 int selected_year = 0;
7644 int selected_month = 0;
7645 int selected_day = 0;
7646 if (selected_date == NULL || sscanf(selected_date, "%d-%d-%d", &selected_year, &selected_month, &selected_day) != 3) {
7647 selected_year = year;
7648 selected_month = month;
7649 selected_day = 1;
7650 }
7651 {
7652 const char *selected_day_name = day_abbrev_for_ymd_runtime(selected_year, selected_month, selected_day);
7653 int schedule_found = 0;
7654 if (availability_rows != NULL && yyjson_arr_iter_init(availability_rows, &iter)) {
7655 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
7656 char *row_day = NULL;
7657 char *start = NULL;
7658 char *end = NULL;
7659 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
7660 continue;
7661 }
7662 row_day = native_json_obj_get_string_dup(row, "day_of_week");
7663 start = native_json_obj_get_string_dup(row, "start_time");
7664 end = native_json_obj_get_string_dup(row, "end_time");
7665 if (row_day != NULL && streq(row_day, selected_day_name)) {
7666 if (!schedule_found && !text_buffer_append_text_runtime(&selected_schedule, "<ul>")) {
7667 free(row_day); free(start); free(end);
7668 goto business_calendar_render_cleanup;
7669 }
7670 schedule_found = 1;
7671 if (!text_buffer_append_text_runtime(&selected_schedule, "<li><strong>") ||
7672 !text_buffer_append_html_escaped_runtime(&selected_schedule, start != NULL ? start : "") ||
7673 !text_buffer_append_text_runtime(&selected_schedule, "–") ||
7674 !text_buffer_append_html_escaped_runtime(&selected_schedule, end != NULL ? end : "") ||
7675 !text_buffer_append_text_runtime(&selected_schedule, "</strong> <span class=\"muted\">business hours</span></li>")) {
7676 free(row_day); free(start); free(end);
7677 goto business_calendar_render_cleanup;
7678 }
7679 }
7680 free(row_day); free(start); free(end);
7681 }
7682 }
7683 if (!schedule_found) {
7684 if (!text_buffer_append_text_runtime(&selected_schedule, "<p class=\"muted\">This business is closed on the selected day.</p>")) {
7685 goto business_calendar_render_cleanup;
7686 }
7687 } else if (!text_buffer_append_text_runtime(&selected_schedule, "</ul><p style=\"margin-top:0.75rem;\"><a class=\"btn btn-secondary\" href=\"/b/") ||
7688 !text_buffer_append_html_escaped_runtime(&selected_schedule, slug != NULL ? slug : "") ||
7689 !text_buffer_append_text_runtime(&selected_schedule, "/\">Open public booking page</a></p>")) {
7690 goto business_calendar_render_cleanup;
7691 }
7692 }
7693 }
7694 if (appointments != NULL && yyjson_arr_iter_init(appointments, &iter)) {
7695 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
7696 yyjson_val *service = NULL;
7697 yyjson_mut_doc *doc = NULL;
7698 yyjson_mut_val *params = NULL;
7699 char *row_date = NULL;
7700 char *status = NULL;
7701 char *service_name = NULL;
7702 char *html = NULL;
7703 long appointment_id = 0;
7704 long service_id = 0;
7705 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
7706 continue;
7707 }
7708 row_date = native_json_obj_get_string_dup(row, "date");
7709 if (row_date == NULL || !streq(row_date, selected_date != NULL ? selected_date : "")) {
7710 free(row_date);
7711 continue;
7712 }
7713 appointment_id = native_json_obj_get_long_default(row, "id", 0, NULL);
7714 service_id = native_json_obj_get_long_default(row, "service_id", 0, NULL);
7715 service = state_find_service_by_id_runtime(root, service_id);
7716 status = native_json_obj_get_string_dup(row, "status");
7717 service_name = service != NULL ? native_json_obj_get_string_dup(service, "name") : NULL;
7718 doc = yyjson_mut_doc_new(NULL);
7719 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
7720 if (doc == NULL || params == NULL) {
7721 free(row_date); free(status); free(service_name); yyjson_mut_doc_free(doc);
7722 goto business_calendar_render_cleanup;
7723 }
7724 yyjson_mut_doc_set_root(doc, params);
7725 {
7726 char *time_text = native_json_obj_get_string_dup(row, "time");
7727 yyjson_mut_obj_add_strcpy(doc, params, "service_name", service_name != NULL ? service_name : "");
7728 yyjson_mut_obj_add_strcpy(doc, params, "business_name", business_name != NULL ? business_name : "");
7729 yyjson_mut_obj_add_strcpy(doc, params, "date_value", row_date != NULL ? row_date : "");
7730 yyjson_mut_obj_add_strcpy(doc, params, "time_value", time_text != NULL ? time_text : "");
7731 yyjson_mut_obj_add_strcpy(doc, params, "status_value", status != NULL ? status : "");
7732 free(time_text);
7733 }
7734 {
7735 char action_path[256];
7736 snprintf(action_path, sizeof(action_path), "/appointments/%ld/complete/", appointment_id);
7737 yyjson_mut_obj_add_strcpy(doc, params, "complete_path", action_path);
7738 snprintf(action_path, sizeof(action_path), "/appointments/%ld/cancel/", appointment_id);
7739 yyjson_mut_obj_add_strcpy(doc, params, "cancel_path", action_path);
7740 }
7741 html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/calendar/fragments/managed_appointment_row.html", doc);
7743 free(row_date); free(status); free(service_name);
7744 if (html == NULL || !text_buffer_append_text_runtime(&appointment_rows, html)) {
7745 free(html);
7746 goto business_calendar_render_cleanup;
7747 }
7748 free(html);
7749 }
7750 }
7751 if (appointment_rows.length == 0 && !text_buffer_append_text_runtime(&appointment_rows, "<li class=\"card\"><p>No appointments on this day.</p></li>")) {
7752 goto business_calendar_render_cleanup;
7753 }
7754 {
7755 size_t needed = strlen("/b//") + strlen(slug != NULL ? slug : "") + 1;
7756 landing_path = (char *)malloc(needed);
7757 if (landing_path == NULL) {
7758 goto business_calendar_render_cleanup;
7759 }
7760 snprintf(landing_path, needed, "/b/%s/", slug != NULL ? slug : "");
7761 }
7762 {
7763 size_t needed = strlen("/businesses//") + strlen(slug != NULL ? slug : "") + 1;
7764 manage_path = (char *)malloc(needed);
7765 if (manage_path == NULL) {
7766 goto business_calendar_render_cleanup;
7767 }
7768 snprintf(manage_path, needed, "/businesses/%s/", slug != NULL ? slug : "");
7769 }
7770 {
7771 char buffer[512];
7772 snprintf(buffer, sizeof(buffer), "/business/%s/calendar/?year=%04d&month=%02d", slug != NULL ? slug : "", prev_year, prev_month);
7773 prev_month_path = strdup(buffer);
7774 snprintf(buffer, sizeof(buffer), "/business/%s/calendar/?year=%04d&month=%02d", slug != NULL ? slug : "", next_year, next_month);
7775 next_month_path = strdup(buffer);
7776 if (prev_month_path == NULL || next_month_path == NULL) {
7777 goto business_calendar_render_cleanup;
7778 }
7779 }
7780 page_doc = yyjson_mut_doc_new(NULL);
7781 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
7782 if (page_doc == NULL || page_obj == NULL) {
7783 goto business_calendar_render_cleanup;
7784 }
7785 yyjson_mut_doc_set_root(page_doc, page_obj);
7786 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "business_name", business_name != NULL ? business_name : "");
7787 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "landing_path", landing_path != NULL ? landing_path : "");
7788 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "manage_path", manage_path != NULL ? manage_path : "");
7789 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "prev_month_path", prev_month_path != NULL ? prev_month_path : "");
7790 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "next_month_path", next_month_path != NULL ? next_month_path : "");
7791 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "month_label", month_name_runtime(month));
7792 yyjson_mut_obj_add_int(page_doc, page_obj, "selected_year", year);
7793 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "selected_date", selected_date != NULL ? selected_date : "");
7794 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "calendar_rows_html", calendar_rows.text != NULL ? calendar_rows.text : "");
7795 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "selected_schedule_html", selected_schedule.text != NULL ? selected_schedule.text : "");
7796 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "appointment_rows_html", appointment_rows.text != NULL ? appointment_rows.text : "");
7797 code = emit_compact_json_mut_value_runtime(page_obj);
7798
7799business_calendar_render_cleanup:
7800 text_buffer_free_runtime(&calendar_rows);
7801 text_buffer_free_runtime(&week_row);
7802 text_buffer_free_runtime(&selected_schedule);
7803 text_buffer_free_runtime(&appointment_rows);
7804 free(business_name);
7805 free(landing_path);
7806 free(manage_path);
7807 free(prev_month_path);
7808 free(next_month_path);
7809 free(selected_date);
7810 yyjson_mut_doc_free(page_doc);
7811 return code;
7812}
7813
7815 const char *state_path,
7816 const char *business_slug,
7817 const char *app_root,
7818 const char *selected_year_text,
7819 const char *selected_month_text,
7820 const char *selected_date_text
7821) {
7822 yyjson_doc *state_doc = NULL;
7823 yyjson_val *root = NULL;
7824 yyjson_val *business = NULL;
7825 yyjson_val *availability_rows = NULL;
7826 yyjson_val *appointments = NULL;
7827 yyjson_mut_doc *page_doc = NULL;
7828 yyjson_mut_val *page_obj = NULL;
7829 yyjson_arr_iter iter;
7830 yyjson_val *row = NULL;
7831 TextBufferRuntime calendar_rows = {0};
7832 TextBufferRuntime week_row = {0};
7833 TextBufferRuntime selected_schedule = {0};
7834 TextBufferRuntime appointment_rows = {0};
7835 char *business_name = NULL;
7836 char *landing_path = NULL;
7837 char *manage_path = NULL;
7838 char *prev_month_path = NULL;
7839 char *next_month_path = NULL;
7840 char *selected_date = NULL;
7841 char *result = NULL;
7842 long business_id = 0;
7843 int year = 0;
7844 int month = 0;
7845 int prev_year = 0;
7846 int prev_month = 0;
7847 int next_year = 0;
7848 int next_month = 0;
7849 int month_days = 0;
7850 int first_offset = 0;
7851 int day_cell_count = 0;
7852 int day_number = 1;
7853 time_t now = time(NULL);
7854 struct tm *local = localtime(&now);
7855
7856 if (state_path == NULL || state_path[0] == '\0'
7857 || business_slug == NULL || business_slug[0] == '\0'
7858 || app_root == NULL || app_root[0] == '\0') {
7859 return NULL;
7860 }
7861
7862 state_doc = native_json_doc_load_file(state_path);
7863 root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
7864 business = root != NULL ? state_find_business_by_slug_runtime(root, business_slug) : NULL;
7865 availability_rows = root != NULL ? state_array_runtime(root, "business_availability") : NULL;
7866 appointments = root != NULL ? state_array_runtime(root, "appointments") : NULL;
7867 if (business == NULL) {
7868 native_json_doc_free(state_doc);
7869 return NULL;
7870 }
7871
7872 business_id = native_json_obj_get_long_default(business, "id", 0, NULL);
7873 business_name = native_json_obj_get_string_dup(business, "name");
7874 year = (selected_year_text != NULL && selected_year_text[0] != '\0') ? (int)strtol(selected_year_text, NULL, 10) : (local != NULL ? local->tm_year + 1900 : 1970);
7875 month = (selected_month_text != NULL && selected_month_text[0] != '\0') ? (int)strtol(selected_month_text, NULL, 10) : (local != NULL ? local->tm_mon + 1 : 1);
7876 if (year < 1970) year = 1970;
7877 if (month < 1) month = 1;
7878 if (month > 12) month = 12;
7879 if (selected_date_text != NULL && selected_date_text[0] != '\0') {
7880 selected_date = strdup(selected_date_text);
7881 } else {
7882 TextBufferRuntime date_buffer = {0};
7883 if (!append_date_runtime(&date_buffer, year, month, 1)) {
7884 text_buffer_free_runtime(&date_buffer);
7885 goto business_calendar_params_cleanup;
7886 }
7887 selected_date = text_buffer_take_runtime(&date_buffer);
7888 }
7889 month_shift_runtime(year, month, -1, &prev_year, &prev_month);
7890 month_shift_runtime(year, month, 1, &next_year, &next_month);
7891 month_days = days_in_month_runtime(year, month);
7892 first_offset = weekday_monday_index_runtime(year, month, 1) - 1;
7893 while (day_cell_count < first_offset) {
7894 if (!text_buffer_append_text_runtime(&week_row, "<td class=\"day\"></td>")) {
7895 goto business_calendar_params_cleanup;
7896 }
7897 day_cell_count++;
7898 }
7899 while (day_number <= month_days) {
7900 TextBufferRuntime date_value = {0};
7901 const char *day_name = NULL;
7902 int available_count = 0;
7903 int appointment_count = 0;
7904 char *date_text = NULL;
7905 int selected = 0;
7906 if (!append_date_runtime(&date_value, year, month, day_number)) {
7907 text_buffer_free_runtime(&date_value);
7908 goto business_calendar_params_cleanup;
7909 }
7910 date_text = text_buffer_take_runtime(&date_value);
7911 day_name = day_abbrev_for_ymd_runtime(year, month, day_number);
7912 if (availability_rows != NULL && yyjson_arr_iter_init(availability_rows, &iter)) {
7913 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
7914 char *row_day = NULL;
7915 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
7916 continue;
7917 }
7918 row_day = native_json_obj_get_string_dup(row, "day_of_week");
7919 if (row_day != NULL && streq(row_day, day_name)) {
7920 available_count++;
7921 }
7922 free(row_day);
7923 }
7924 }
7925 if (appointments != NULL && yyjson_arr_iter_init(appointments, &iter)) {
7926 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
7927 char *row_date = NULL;
7928 char *status = NULL;
7929 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
7930 continue;
7931 }
7932 row_date = native_json_obj_get_string_dup(row, "date");
7933 status = native_json_obj_get_string_dup(row, "status");
7934 if (row_date != NULL && streq(row_date, date_text) && !(status != NULL && streq(status, "cancelled"))) {
7935 appointment_count++;
7936 }
7937 free(row_date);
7938 free(status);
7939 }
7940 }
7941 selected = selected_date != NULL && streq(selected_date, date_text);
7942 if (!text_buffer_append_format_runtime(&week_row, "<td class=\"day%s%s\" style=\"vertical-align:top;padding:0.5rem;min-width:88px;\">", available_count == 0 ? " unavailable" : "", selected ? " today" : "") ||
7943 !text_buffer_append_format_runtime(&week_row, "<a href=\"/business/%s/calendar/?year=%04d&month=%02d&date=%s\" class=\"btn calendar-day-chip\" style=\"display:inline-block;min-width:2.25rem;text-align:center;%s\">%d</a>", business_slug, year, month, date_text, selected ? "background:var(--primary);color:#fff;" : "background:var(--gray-200);color:var(--gray-700);", day_number)) {
7944 free(date_text);
7945 goto business_calendar_params_cleanup;
7946 }
7947 if (appointment_count > 0 && !text_buffer_append_format_runtime(&week_row, "<div style=\"margin-top:0.4rem;font-size:0.75rem;color:var(--gray-700);\">%d booked</div>", appointment_count)) {
7948 free(date_text);
7949 goto business_calendar_params_cleanup;
7950 }
7951 if (available_count > 0) {
7952 if (!text_buffer_append_format_runtime(&week_row, "<div style=\"margin-top:0.2rem;font-size:0.75rem;color:var(--primary);\">%d slots</div>", available_count)) {
7953 free(date_text);
7954 goto business_calendar_params_cleanup;
7955 }
7956 } else if (!text_buffer_append_text_runtime(&week_row, "<div style=\"margin-top:0.2rem;font-size:0.75rem;color:var(--gray-500);\">No slots</div>")) {
7957 free(date_text);
7958 goto business_calendar_params_cleanup;
7959 }
7960 if (!text_buffer_append_text_runtime(&week_row, "</td>")) {
7961 free(date_text);
7962 goto business_calendar_params_cleanup;
7963 }
7964 free(date_text);
7965 day_cell_count++;
7966 if ((day_cell_count % 7) == 0) {
7967 if (!text_buffer_append_text_runtime(&calendar_rows, "<tr>") || !text_buffer_append_text_runtime(&calendar_rows, week_row.text != NULL ? week_row.text : "") || !text_buffer_append_text_runtime(&calendar_rows, "</tr>")) {
7968 goto business_calendar_params_cleanup;
7969 }
7970 text_buffer_free_runtime(&week_row);
7971 }
7972 day_number++;
7973 }
7974 while ((day_cell_count % 7) != 0) {
7975 if (!text_buffer_append_text_runtime(&week_row, "<td class=\"day\"></td>")) {
7976 goto business_calendar_params_cleanup;
7977 }
7978 day_cell_count++;
7979 }
7980 if (week_row.length > 0) {
7981 if (!text_buffer_append_text_runtime(&calendar_rows, "<tr>") || !text_buffer_append_text_runtime(&calendar_rows, week_row.text != NULL ? week_row.text : "") || !text_buffer_append_text_runtime(&calendar_rows, "</tr>")) {
7982 goto business_calendar_params_cleanup;
7983 }
7984 }
7985 {
7986 int selected_year = 0;
7987 int selected_month = 0;
7988 int selected_day = 0;
7989 if (selected_date == NULL || sscanf(selected_date, "%d-%d-%d", &selected_year, &selected_month, &selected_day) != 3) {
7990 selected_year = year;
7991 selected_month = month;
7992 selected_day = 1;
7993 }
7994 {
7995 const char *selected_day_name = day_abbrev_for_ymd_runtime(selected_year, selected_month, selected_day);
7996 int schedule_found = 0;
7997 if (availability_rows != NULL && yyjson_arr_iter_init(availability_rows, &iter)) {
7998 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
7999 char *row_day = NULL;
8000 char *start = NULL;
8001 char *end = NULL;
8002 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
8003 continue;
8004 }
8005 row_day = native_json_obj_get_string_dup(row, "day_of_week");
8006 start = native_json_obj_get_string_dup(row, "start_time");
8007 end = native_json_obj_get_string_dup(row, "end_time");
8008 if (row_day != NULL && streq(row_day, selected_day_name)) {
8009 if (!schedule_found && !text_buffer_append_text_runtime(&selected_schedule, "<ul>")) {
8010 free(row_day); free(start); free(end);
8011 goto business_calendar_params_cleanup;
8012 }
8013 schedule_found = 1;
8014 if (!text_buffer_append_text_runtime(&selected_schedule, "<li><strong>") ||
8015 !text_buffer_append_html_escaped_runtime(&selected_schedule, start != NULL ? start : "") ||
8016 !text_buffer_append_text_runtime(&selected_schedule, "–") ||
8017 !text_buffer_append_html_escaped_runtime(&selected_schedule, end != NULL ? end : "") ||
8018 !text_buffer_append_text_runtime(&selected_schedule, "</strong> <span class=\"muted\">business hours</span></li>")) {
8019 free(row_day); free(start); free(end);
8020 goto business_calendar_params_cleanup;
8021 }
8022 }
8023 free(row_day); free(start); free(end);
8024 }
8025 }
8026 if (!schedule_found) {
8027 if (!text_buffer_append_text_runtime(&selected_schedule, "<p class=\"muted\">This business is closed on the selected day.</p>")) {
8028 goto business_calendar_params_cleanup;
8029 }
8030 } else if (!text_buffer_append_text_runtime(&selected_schedule, "</ul><p style=\"margin-top:0.75rem;\"><a class=\"btn btn-secondary\" href=\"/b/") ||
8031 !text_buffer_append_html_escaped_runtime(&selected_schedule, business_slug) ||
8032 !text_buffer_append_text_runtime(&selected_schedule, "/\">Open public booking page</a></p>")) {
8033 goto business_calendar_params_cleanup;
8034 }
8035 }
8036 }
8037 if (appointments != NULL && yyjson_arr_iter_init(appointments, &iter)) {
8038 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
8039 yyjson_val *service = NULL;
8040 yyjson_mut_doc *doc = NULL;
8041 yyjson_mut_val *params = NULL;
8042 char *row_date = NULL;
8043 char *status = NULL;
8044 char *service_name = NULL;
8045 char *html = NULL;
8046 long appointment_id = 0;
8047 long service_id = 0;
8048 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
8049 continue;
8050 }
8051 row_date = native_json_obj_get_string_dup(row, "date");
8052 if (row_date == NULL || !streq(row_date, selected_date != NULL ? selected_date : "")) {
8053 free(row_date);
8054 continue;
8055 }
8056 appointment_id = native_json_obj_get_long_default(row, "id", 0, NULL);
8057 service_id = native_json_obj_get_long_default(row, "service_id", 0, NULL);
8058 service = state_find_service_by_id_runtime(root, service_id);
8059 status = native_json_obj_get_string_dup(row, "status");
8060 service_name = service != NULL ? native_json_obj_get_string_dup(service, "name") : NULL;
8061 doc = yyjson_mut_doc_new(NULL);
8062 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
8063 if (doc == NULL || params == NULL) {
8064 free(row_date); free(status); free(service_name); yyjson_mut_doc_free(doc);
8065 goto business_calendar_params_cleanup;
8066 }
8067 yyjson_mut_doc_set_root(doc, params);
8068 {
8069 char *time_text = native_json_obj_get_string_dup(row, "time");
8070 yyjson_mut_obj_add_strcpy(doc, params, "service_name", service_name != NULL ? service_name : "");
8071 yyjson_mut_obj_add_strcpy(doc, params, "business_name", business_name != NULL ? business_name : "");
8072 yyjson_mut_obj_add_strcpy(doc, params, "date_value", row_date != NULL ? row_date : "");
8073 yyjson_mut_obj_add_strcpy(doc, params, "time_value", time_text != NULL ? time_text : "");
8074 yyjson_mut_obj_add_strcpy(doc, params, "status_value", status != NULL ? status : "");
8075 free(time_text);
8076 }
8077 {
8078 char action_path[256];
8079 snprintf(action_path, sizeof(action_path), "/appointments/%ld/complete/", appointment_id);
8080 yyjson_mut_obj_add_strcpy(doc, params, "complete_path", action_path);
8081 snprintf(action_path, sizeof(action_path), "/appointments/%ld/cancel/", appointment_id);
8082 yyjson_mut_obj_add_strcpy(doc, params, "cancel_path", action_path);
8083 }
8084 html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/calendar/fragments/managed_appointment_row.html", doc);
8086 free(row_date); free(status); free(service_name);
8087 if (html == NULL || !text_buffer_append_text_runtime(&appointment_rows, html)) {
8088 free(html);
8089 goto business_calendar_params_cleanup;
8090 }
8091 free(html);
8092 }
8093 }
8094 if (appointment_rows.length == 0 && !text_buffer_append_text_runtime(&appointment_rows, "<li class=\"card\"><p>No appointments on this day.</p></li>")) {
8095 goto business_calendar_params_cleanup;
8096 }
8097 {
8098 char path_buffer[256];
8099 snprintf(path_buffer, sizeof(path_buffer), "/b/%s/", business_slug);
8100 landing_path = strdup(path_buffer);
8101 snprintf(path_buffer, sizeof(path_buffer), "/businesses/%s/", business_slug);
8102 manage_path = strdup(path_buffer);
8103 snprintf(path_buffer, sizeof(path_buffer), "/business/%s/calendar/?year=%04d&month=%02d", business_slug, prev_year, prev_month);
8104 prev_month_path = strdup(path_buffer);
8105 snprintf(path_buffer, sizeof(path_buffer), "/business/%s/calendar/?year=%04d&month=%02d", business_slug, next_year, next_month);
8106 next_month_path = strdup(path_buffer);
8107 if (landing_path == NULL || manage_path == NULL || prev_month_path == NULL || next_month_path == NULL) {
8108 goto business_calendar_params_cleanup;
8109 }
8110 }
8111 page_doc = yyjson_mut_doc_new(NULL);
8112 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
8113 if (page_doc == NULL || page_obj == NULL) {
8114 goto business_calendar_params_cleanup;
8115 }
8116 yyjson_mut_doc_set_root(page_doc, page_obj);
8117 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "business_name", business_name != NULL ? business_name : "");
8118 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "landing_path", landing_path != NULL ? landing_path : "");
8119 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "manage_path", manage_path != NULL ? manage_path : "");
8120 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "prev_month_path", prev_month_path != NULL ? prev_month_path : "");
8121 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "next_month_path", next_month_path != NULL ? next_month_path : "");
8122 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "month_label", month_name_runtime(month));
8123 yyjson_mut_obj_add_int(page_doc, page_obj, "selected_year", year);
8124 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "selected_date", selected_date != NULL ? selected_date : "");
8125 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "calendar_rows_html", calendar_rows.text != NULL ? calendar_rows.text : "");
8126 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "selected_schedule_html", selected_schedule.text != NULL ? selected_schedule.text : "");
8127 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "appointment_rows_html", appointment_rows.text != NULL ? appointment_rows.text : "");
8128 result = yyjson_mut_write(page_doc, YYJSON_WRITE_NOFLAG, NULL);
8129
8130business_calendar_params_cleanup:
8131 text_buffer_free_runtime(&calendar_rows);
8132 text_buffer_free_runtime(&week_row);
8133 text_buffer_free_runtime(&selected_schedule);
8134 text_buffer_free_runtime(&appointment_rows);
8135 free(business_name);
8136 free(landing_path);
8137 free(manage_path);
8138 free(prev_month_path);
8139 free(next_month_path);
8140 free(selected_date);
8141 yyjson_mut_doc_free(page_doc);
8142 native_json_doc_free(state_doc);
8143 return result;
8144}
8145
8146static int emit_business_availability_lines_runtime(yyjson_val *root, long business_id, const char *day_name) {
8147 yyjson_arr_iter iter;
8148 yyjson_val *rows = state_array_runtime(root, "business_availability");
8149 yyjson_val *row = NULL;
8150 if (rows == NULL || day_name == NULL || day_name[0] == '\0' || !yyjson_arr_iter_init(rows, &iter)) {
8151 return 0;
8152 }
8153 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
8154 char *row_day = NULL;
8155 char *start_time = NULL;
8156 char *end_time = NULL;
8157 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
8158 continue;
8159 }
8160 row_day = native_json_obj_get_string_dup(row, "day_of_week");
8161 if (row_day == NULL || !streq(row_day, day_name)) {
8162 free(row_day);
8163 continue;
8164 }
8165 start_time = native_json_obj_get_string_dup(row, "start_time");
8166 end_time = native_json_obj_get_string_dup(row, "end_time");
8167 if (start_time != NULL && end_time != NULL) {
8168 fprintf(stdout, "%s|%s\n", start_time, end_time);
8169 }
8170 free(row_day);
8171 free(start_time);
8172 free(end_time);
8173 }
8174 return 0;
8175}
8176
8177static int emit_technician_availability_lines_runtime(yyjson_val *root, long technician_id, const char *day_name) {
8178 yyjson_arr_iter iter;
8179 yyjson_val *rows = state_array_runtime(root, "technician_availability");
8180 yyjson_val *row = NULL;
8181 if (rows == NULL || day_name == NULL || day_name[0] == '\0' || !yyjson_arr_iter_init(rows, &iter)) {
8182 return 0;
8183 }
8184 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
8185 char *row_day = NULL;
8186 char *start_time = NULL;
8187 char *end_time = NULL;
8188 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "technician_id"), technician_id)) {
8189 continue;
8190 }
8191 row_day = native_json_obj_get_string_dup(row, "day_of_week");
8192 if (row_day == NULL || !streq(row_day, day_name)) {
8193 free(row_day);
8194 continue;
8195 }
8196 start_time = native_json_obj_get_string_dup(row, "start_time");
8197 end_time = native_json_obj_get_string_dup(row, "end_time");
8198 if (start_time != NULL && end_time != NULL) {
8199 fprintf(stdout, "%s|%s\n", start_time, end_time);
8200 }
8201 free(row_day);
8202 free(start_time);
8203 free(end_time);
8204 }
8205 return 0;
8206}
8207
8208static int emit_technician_appointments_lines_runtime(yyjson_val *root, long technician_id, const char *date_value) {
8209 yyjson_arr_iter iter;
8210 yyjson_val *rows = state_array_runtime(root, "appointments");
8211 yyjson_val *row = NULL;
8212 if (rows == NULL || date_value == NULL || date_value[0] == '\0' || !yyjson_arr_iter_init(rows, &iter)) {
8213 return 0;
8214 }
8215 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
8216 char *row_date = NULL;
8217 char *time_value = NULL;
8218 char *status_value = NULL;
8219 long service_id = 0;
8220 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "technician_id"), technician_id)) {
8221 continue;
8222 }
8223 row_date = native_json_obj_get_string_dup(row, "date");
8224 if (row_date == NULL || !streq(row_date, date_value)) {
8225 free(row_date);
8226 continue;
8227 }
8228 time_value = native_json_obj_get_string_dup(row, "time");
8229 status_value = native_json_obj_get_string_dup(row, "status");
8230 service_id = native_json_obj_get_long_default(row, "service_id", 0, NULL);
8231 if (time_value != NULL && status_value != NULL) {
8232 fprintf(stdout, "%s|%ld|%s\n", time_value, service_id, status_value);
8233 }
8234 free(row_date);
8235 free(time_value);
8236 free(status_value);
8237 }
8238 return 0;
8239}
8240
8242 yyjson_arr_iter iter;
8243 yyjson_val *rows = state_array_runtime(root, "tenants");
8244 yyjson_val *row = NULL;
8245 if (rows == NULL || !yyjson_arr_iter_init(rows, &iter)) {
8246 return 0;
8247 }
8248 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
8249 yyjson_val *active_value = NULL;
8250 if (!yyjson_is_obj(row)) {
8251 continue;
8252 }
8253 active_value = native_json_obj_get(row, "is_active");
8254 if (yyjson_is_bool(active_value) && !yyjson_get_bool(active_value)) {
8255 continue;
8256 }
8257 fprintf(stdout, "%ld\n", native_json_obj_get_long_default(row, "id", 0, NULL));
8258 }
8259 return 0;
8260}
8261
8263 yyjson_arr_iter iter;
8264 yyjson_val *rows = state_array_runtime(root, "tenant_memberships");
8265 yyjson_val *row = NULL;
8266 int first = 1;
8267 if (rows == NULL || !yyjson_arr_iter_init(rows, &iter)) {
8268 fputs("[]", stdout);
8269 return 0;
8270 }
8271 fputc('[', stdout);
8272 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
8273 char *membership_status = NULL;
8274 long tenant_id = 0;
8275 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "user_id"), user_id)) {
8276 continue;
8277 }
8278 membership_status = native_json_obj_get_string_dup(row, "membership_status");
8279 if (membership_status != NULL && membership_status[0] != '\0' && !streq(membership_status, "active")) {
8280 free(membership_status);
8281 continue;
8282 }
8283 free(membership_status);
8284 tenant_id = native_json_obj_get_long_default(row, "tenant_id", 0, NULL);
8285 if (!first) {
8286 fputc(',', stdout);
8287 }
8288 fprintf(stdout, "%ld", tenant_id);
8289 first = 0;
8290 }
8291 fputc(']', stdout);
8292 return 0;
8293}
8294
8295static int json_array_contains_long_runtime(yyjson_val *array_value, long wanted) {
8296 yyjson_arr_iter iter;
8297 yyjson_val *item = NULL;
8298 if (array_value == NULL || !yyjson_is_arr(array_value) || !yyjson_arr_iter_init(array_value, &iter)) {
8299 return 0;
8300 }
8301 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
8302 long value = 0;
8303 if (long_from_value_runtime(item, &value) && value == wanted) {
8304 return 1;
8305 }
8306 }
8307 return 0;
8308}
8309
8310static long tenant_id_for_business_runtime(yyjson_val *root, long business_id) {
8311 return state_tenant_id_for_business_id_runtime(root, business_id);
8312}
8313
8315 yyjson_val *businesses = state_array_runtime(root, "businesses");
8316 yyjson_arr_iter iter;
8317 yyjson_val *row = NULL;
8318 yyjson_mut_doc *mut_doc = NULL;
8319 yyjson_mut_val *array = NULL;
8320 int code = 69;
8321 if (businesses == NULL || !yyjson_arr_iter_init(businesses, &iter)) {
8322 fputs("[]", stdout);
8323 return 0;
8324 }
8325 mut_doc = yyjson_mut_doc_new(NULL);
8326 array = mut_doc != NULL ? yyjson_mut_arr(mut_doc) : NULL;
8327 if (mut_doc == NULL || array == NULL) {
8328 yyjson_mut_doc_free(mut_doc);
8329 return 69;
8330 }
8331 yyjson_mut_doc_set_root(mut_doc, array);
8332 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
8333 yyjson_mut_val *obj = NULL;
8334 char *name = NULL;
8335 char *description = NULL;
8336 char *slug = NULL;
8337 long business_id = 0;
8338 long tenant_id = 0;
8339 if (!yyjson_is_obj(row)) {
8340 continue;
8341 }
8342 business_id = native_json_obj_get_long_default(row, "id", 0, NULL);
8343 tenant_id = tenant_id_for_business_runtime(root, business_id);
8344 if (!json_array_contains_long_runtime(tenant_ids, tenant_id)) {
8345 continue;
8346 }
8347 name = native_json_obj_get_string_dup(row, "name");
8348 description = native_json_obj_get_string_dup(row, "description");
8349 slug = native_json_obj_get_string_dup(row, "slug");
8350 obj = yyjson_mut_obj(mut_doc);
8351 if (obj == NULL
8352 || !yyjson_mut_obj_add_int(mut_doc, obj, "id", business_id)
8353 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "name", name != NULL ? name : "")
8354 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "description", description != NULL ? description : "")
8355 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "slug", slug != NULL ? slug : "")
8356 || !yyjson_mut_arr_append(array, obj)) {
8357 free(name);
8358 free(description);
8359 free(slug);
8360 yyjson_mut_doc_free(mut_doc);
8361 return 69;
8362 }
8363 free(name);
8364 free(description);
8365 free(slug);
8366 }
8368 yyjson_mut_doc_free(mut_doc);
8369 return code;
8370}
8371
8372static char *render_business_fragment_dup_runtime(const char *app_root, const char *fragment_name, yyjson_val *params_root) {
8373 char *template_file = join_base_relative_path_dup_runtime(app_root, fragment_name);
8374 char *rendered = NULL;
8375 if (template_file == NULL) {
8376 return NULL;
8377 }
8378 rendered = render_file_template_dup_runtime(template_file, params_root);
8379 free(template_file);
8380 return rendered;
8381}
8382
8383static char *render_business_fragment_from_doc_dup_runtime(const char *app_root, const char *fragment_name, yyjson_mut_doc *doc) {
8384 char *params_json = NULL;
8385 yyjson_doc *params_doc = NULL;
8386 yyjson_val *params_root = NULL;
8387 char *rendered = NULL;
8388 if (doc == NULL) {
8389 return NULL;
8390 }
8391 params_json = yyjson_mut_write(doc, YYJSON_WRITE_NOFLAG, NULL);
8392 if (params_json == NULL) {
8393 return NULL;
8394 }
8395 params_root = load_root_from_text_runtime(params_json, &params_doc);
8396 free(params_json);
8397 if (params_root == NULL || !yyjson_is_obj(params_root)) {
8398 native_json_doc_free(params_doc);
8399 return NULL;
8400 }
8401 rendered = render_business_fragment_dup_runtime(app_root, fragment_name, params_root);
8402 native_json_doc_free(params_doc);
8403 return rendered;
8404}
8405
8406static char *render_business_admin_empty_state_dup_runtime(const char *app_root, const char *message) {
8408 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
8409 char *html = NULL;
8410 if (doc == NULL || params == NULL) {
8412 return NULL;
8413 }
8414 yyjson_mut_doc_set_root(doc, params);
8415 yyjson_mut_obj_add_strcpy(doc, params, "message", message != NULL ? message : "");
8416 html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/business_admin_empty_state.html", doc);
8418 return html;
8419}
8420
8421static char *service_abbreviation_dup_runtime(const char *name) {
8422 char letters[4] = {0};
8423 size_t count = 0;
8424 int in_token = 0;
8425 size_t index = 0;
8426 if (name == NULL) {
8427 return strdup("XX");
8428 }
8429 for (index = 0; name[index] != '\0' && count < 3; index++) {
8430 unsigned char ch = (unsigned char)name[index];
8431 if (isalnum(ch)) {
8432 if (!in_token) {
8433 letters[count++] = (char)toupper(ch);
8434 in_token = 1;
8435 }
8436 } else {
8437 in_token = 0;
8438 }
8439 }
8440 if (count < 2) {
8441 count = 0;
8442 for (index = 0; name[index] != '\0' && count < 2; index++) {
8443 unsigned char ch = (unsigned char)name[index];
8444 if (isalnum(ch)) {
8445 letters[count++] = (char)toupper(ch);
8446 }
8447 }
8448 }
8449 while (count < 2) {
8450 letters[count++] = 'X';
8451 }
8452 letters[count] = '\0';
8453 return strdup(letters);
8454}
8455
8456static const char *service_badge_color_runtime(const char *name) {
8457 static const char *colors[] = { "#2563eb", "#7c3aed", "#0f766e", "#b45309", "#be123c", "#0369a1" };
8458 unsigned long checksum = 0;
8459 size_t index = 0;
8460 if (name != NULL) {
8461 for (index = 0; name[index] != '\0'; index++) {
8462 checksum = (checksum * 33u) + (unsigned char)name[index];
8463 }
8464 }
8465 return colors[checksum % (sizeof(colors) / sizeof(colors[0]))];
8466}
8467
8468/**
8469 * Emit declared-page params for the UCAL public service booking form.
8470 * Bridges service state plus bounded presentation defaults into app-owned templates.
8471 */
8472static int emit_service_book_form_page_json_runtime(yyjson_val *root, const char *slug, long service_id, const char *app_root, const char *error_message) {
8473 yyjson_val *business = state_find_business_by_slug_runtime(root, slug);
8474 yyjson_val *service = state_find_service_by_id_runtime(root, service_id);
8475 yyjson_val *booking_options = NULL;
8476 yyjson_val *time_format_value = NULL;
8477 yyjson_mut_doc *page_doc = NULL;
8478 yyjson_mut_val *page_obj = NULL;
8479 TextBufferRuntime booking_option_options = {0};
8480 TextBufferRuntime booking_option_select = {0};
8481 TextBufferRuntime time_hour_options = {0};
8482 TextBufferRuntime time_minute_options = {0};
8483 char *service_name = NULL;
8484 char *service_description = NULL;
8485 char *service_image_raw = NULL;
8486 char *service_image_url = NULL;
8487 char *service_visual_html = NULL;
8488 char *error_html = NULL;
8489 char *save_path = NULL;
8490 const char *time_grid_columns = "1fr 1fr 1fr";
8491 const char *time_period_html = "<select name=\"time_period\" required><option value=\"AM\" selected>AM</option><option value=\"PM\">PM</option></select>";
8492 const char *time_format = "am_pm";
8493 long business_id = 0;
8494 long service_duration = 45;
8495 long booking_option_count = 0;
8496 int code = 69;
8497 if (business == NULL || service == NULL || app_root == NULL || app_root[0] == '\0') {
8498 return 64;
8499 }
8500 business_id = native_json_obj_get_long_default(business, "id", 0, NULL);
8501 if (!value_long_equals_runtime(native_json_obj_get(service, "business_id"), business_id)) {
8502 return 64;
8503 }
8504 service_name = native_json_obj_get_string_dup(service, "name");
8505 service_description = native_json_obj_get_string_dup(service, "description");
8506 service_image_raw = native_json_obj_get_string_dup(service, "image_url");
8507 if (service_image_raw == NULL || service_image_raw[0] == '\0') {
8508 free(service_image_raw);
8509 service_image_raw = native_json_obj_get_string_dup(service, "image");
8510 }
8511 service_image_url = service_media_url_dup_runtime(slug, service_image_raw != NULL ? service_image_raw : "");
8512 service_visual_html = render_service_visual_html_dup_runtime(app_root, service_image_url != NULL ? service_image_url : "", service_name != NULL ? service_name : "");
8513 if (service_visual_html == NULL) {
8514 goto service_book_form_cleanup;
8515 }
8516 time_format_value = native_json_obj_get(business, "time_format");
8517 if (yyjson_is_str(time_format_value) && yyjson_get_str(time_format_value) != NULL && yyjson_get_str(time_format_value)[0] != '\0') {
8518 time_format = yyjson_get_str(time_format_value);
8519 }
8520 service_duration = native_json_obj_get_long_default(service, "duration_minutes", 45, NULL);
8521 if (service_duration <= 0) {
8522 service_duration = 45;
8523 }
8524 if (error_message != NULL && error_message[0] != '\0') {
8525 TextBufferRuntime tmp = {0};
8526 if (!text_buffer_append_text_runtime(&tmp, "<p style=\"color:#9f1239;\">")
8527 || !text_buffer_append_html_escaped_runtime(&tmp, error_message)
8528 || !text_buffer_append_text_runtime(&tmp, "</p>")) {
8530 goto service_book_form_cleanup;
8531 }
8532 error_html = text_buffer_take_runtime(&tmp);
8533 }
8534 booking_options = native_json_obj_get_array(service, "booking_options");
8535 {
8536 yyjson_arr_iter iter;
8537 yyjson_val *row = NULL;
8538 char *label = NULL;
8539 char *cost = NULL;
8540 long duration = service_duration;
8541 if (!text_buffer_append_text_runtime(&booking_option_options, "<option value=\"0\">")
8542 || !text_buffer_append_html_escaped_runtime(&booking_option_options, service_name != NULL ? service_name : "")
8543 || !text_buffer_append_text_runtime(&booking_option_options, "</option>")) {
8544 goto service_book_form_cleanup;
8545 }
8546 booking_option_count = 1;
8547 if (booking_options != NULL && yyjson_arr_iter_init(booking_options, &iter)) {
8548 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
8549 label = native_json_obj_get_string_dup(row, "label");
8550 cost = native_json_obj_get_string_dup(row, "cost");
8551 duration = native_json_obj_get_long_default(row, "duration_minutes", 45, NULL);
8552 if (duration <= 0) {
8553 duration = 45;
8554 }
8555 if ((label != NULL && label[0] != '\0') || duration > 0 || (cost != NULL && cost[0] != '\0')) {
8556 if (!text_buffer_append_format_runtime(&booking_option_options, "<option value=\"%ld\">", booking_option_count)
8557 || !text_buffer_append_html_escaped_runtime(&booking_option_options, label != NULL && label[0] != '\0' ? label : "Booking option")
8558 || !text_buffer_append_text_runtime(&booking_option_options, "</option>")) {
8559 free(label);
8560 free(cost);
8561 goto service_book_form_cleanup;
8562 }
8563 booking_option_count++;
8564 }
8565 free(label);
8566 free(cost);
8567 }
8568 }
8569 }
8570 if (booking_option_count > 1) {
8571 if (!text_buffer_append_text_runtime(&booking_option_select, "<div class=\"form-group\"><label for=\"booking-option-index\" class=\"sr-only\">Booking option</label><select id=\"booking-option-index\" name=\"booking_option_index\" class=\"form-control\" required style=\"display:block;width:100%;min-height:2.75rem;appearance:none;-webkit-appearance:none;background-color:#fff;color:#111827;border:1px solid #d1d5db;border-radius:6px;padding:0.5rem 2.75rem 0.5rem 0.75rem;background-image:linear-gradient(45deg, transparent 50%, #374151 50%),linear-gradient(135deg, #374151 50%, transparent 50%);background-position:calc(100% - 18px) calc(50% - 3px),calc(100% - 12px) calc(50% - 3px);background-size:6px 6px,6px 6px;background-repeat:no-repeat;font-weight:500;\">")
8572 || !text_buffer_append_text_runtime(&booking_option_select, booking_option_options.text != NULL ? booking_option_options.text : "")
8573 || !text_buffer_append_text_runtime(&booking_option_select, "</select></div>")) {
8574 goto service_book_form_cleanup;
8575 }
8576 }
8577 if (streq(time_format, "military")) {
8578 long hour = 0;
8579 time_grid_columns = "1fr 1fr";
8580 time_period_html = "";
8581 if (!text_buffer_append_text_runtime(&time_hour_options, "<option value=\"\">Hour</option>")) {
8582 goto service_book_form_cleanup;
8583 }
8584 for (hour = 0; hour <= 23; hour++) {
8585 if (!text_buffer_append_format_runtime(&time_hour_options, "<option value=\"%02ld\">%02ld</option>", hour, hour)) {
8586 goto service_book_form_cleanup;
8587 }
8588 }
8589 } else {
8590 long hour = 1;
8591 if (!text_buffer_append_text_runtime(&time_hour_options, "<option value=\"\">Hour</option>")) {
8592 goto service_book_form_cleanup;
8593 }
8594 for (hour = 1; hour <= 12; hour++) {
8595 if (!text_buffer_append_format_runtime(&time_hour_options, "<option value=\"%02ld\">%02ld</option>", hour, hour)) {
8596 goto service_book_form_cleanup;
8597 }
8598 }
8599 }
8600 if (!text_buffer_append_text_runtime(&time_minute_options, "<option value=\"\">Minute</option>")) {
8601 goto service_book_form_cleanup;
8602 }
8603 {
8604 const int minute_values[] = {0, 10, 20, 30, 40, 50};
8605 size_t minute_index = 0;
8606 for (minute_index = 0; minute_index < (sizeof(minute_values) / sizeof(minute_values[0])); minute_index++) {
8607 if (!text_buffer_append_format_runtime(&time_minute_options, "<option value=\"%02d\">%02d</option>", minute_values[minute_index], minute_values[minute_index])) {
8608 goto service_book_form_cleanup;
8609 }
8610 }
8611 }
8612 {
8613 size_t needed = strlen("/business//book//") + strlen(slug != NULL ? slug : "") + 32;
8614 save_path = (char *)malloc(needed);
8615 if (save_path == NULL) {
8616 goto service_book_form_cleanup;
8617 }
8618 snprintf(save_path, needed, "/business/%s/book/%ld/", slug != NULL ? slug : "", service_id);
8619 }
8620 page_doc = yyjson_mut_doc_new(NULL);
8621 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
8622 if (page_doc == NULL || page_obj == NULL) {
8623 goto service_book_form_cleanup;
8624 }
8625 yyjson_mut_doc_set_root(page_doc, page_obj);
8626 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "service_name", service_name != NULL ? service_name : "");
8627 yyjson_mut_obj_add_int(page_doc, page_obj, "service_duration", service_duration);
8628 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "service_visual_html", service_visual_html != NULL ? service_visual_html : "");
8629 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "service_description", service_description != NULL ? service_description : "");
8630 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "error_html", error_html != NULL ? error_html : "");
8631 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "save_path", save_path != NULL ? save_path : "");
8632 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "booking_option_select_html", booking_option_select.text != NULL ? booking_option_select.text : "");
8633 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "time_grid_columns", time_grid_columns);
8634 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "time_hour_options_html", time_hour_options.text != NULL ? time_hour_options.text : "");
8635 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "time_minute_options_html", time_minute_options.text != NULL ? time_minute_options.text : "");
8636 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "time_period_html", time_period_html != NULL ? time_period_html : "");
8637 code = emit_compact_json_mut_value_runtime(page_obj);
8638
8639service_book_form_cleanup:
8640 text_buffer_free_runtime(&booking_option_options);
8641 text_buffer_free_runtime(&booking_option_select);
8642 text_buffer_free_runtime(&time_hour_options);
8643 text_buffer_free_runtime(&time_minute_options);
8644 free(service_name);
8645 free(service_description);
8646 free(service_image_raw);
8647 free(service_image_url);
8648 free(service_visual_html);
8649 free(error_html);
8650 free(save_path);
8651 yyjson_mut_doc_free(page_doc);
8652 return code;
8653}
8654
8656 const char *state_path,
8657 const char *business_slug,
8658 long service_id,
8659 const char *app_root,
8660 const char *error_message
8661) {
8662 yyjson_doc *state_doc = NULL;
8663 yyjson_val *root = NULL;
8664 yyjson_val *business = NULL;
8665 yyjson_val *service = NULL;
8666 yyjson_val *booking_options = NULL;
8667 yyjson_val *time_format_value = NULL;
8668 yyjson_mut_doc *page_doc = NULL;
8669 yyjson_mut_val *page_obj = NULL;
8670 TextBufferRuntime booking_option_options = {0};
8671 TextBufferRuntime booking_option_select = {0};
8672 TextBufferRuntime time_hour_options = {0};
8673 TextBufferRuntime time_minute_options = {0};
8674 char *service_name = NULL;
8675 char *service_description = NULL;
8676 char *service_image_raw = NULL;
8677 char *service_image_url = NULL;
8678 char *service_visual_html = NULL;
8679 char *error_html = NULL;
8680 char *save_path = NULL;
8681 char *result = NULL;
8682 const char *time_grid_columns = "1fr 1fr 1fr";
8683 const char *time_period_html = "<select name=\"time_period\" required><option value=\"AM\" selected>AM</option><option value=\"PM\">PM</option></select>";
8684 const char *time_format = "am_pm";
8685 long business_id = 0;
8686 long service_duration = 45;
8687 long booking_option_count = 0;
8688
8689 if (state_path == NULL || state_path[0] == '\0' || business_slug == NULL || business_slug[0] == '\0' || app_root == NULL || app_root[0] == '\0') {
8690 return NULL;
8691 }
8692
8693 state_doc = native_json_doc_load_file(state_path);
8694 root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
8695 business = root != NULL ? state_find_business_by_slug_runtime(root, business_slug) : NULL;
8696 service = root != NULL ? state_find_service_by_id_runtime(root, service_id) : NULL;
8697 if (business == NULL || service == NULL) {
8698 native_json_doc_free(state_doc);
8699 return NULL;
8700 }
8701
8702 business_id = native_json_obj_get_long_default(business, "id", 0, NULL);
8703 if (!value_long_equals_runtime(native_json_obj_get(service, "business_id"), business_id)) {
8704 native_json_doc_free(state_doc);
8705 return NULL;
8706 }
8707
8708 service_name = native_json_obj_get_string_dup(service, "name");
8709 service_description = native_json_obj_get_string_dup(service, "description");
8710 service_image_raw = native_json_obj_get_string_dup(service, "image_url");
8711 if (service_image_raw == NULL || service_image_raw[0] == '\0') {
8712 free(service_image_raw);
8713 service_image_raw = native_json_obj_get_string_dup(service, "image");
8714 }
8715 service_image_url = service_media_url_dup_runtime(business_slug, service_image_raw != NULL ? service_image_raw : "");
8716 service_visual_html = render_service_visual_html_dup_runtime(app_root, service_image_url != NULL ? service_image_url : "", service_name != NULL ? service_name : "");
8717 if (service_visual_html == NULL) {
8718 goto service_book_form_params_cleanup;
8719 }
8720
8721 time_format_value = native_json_obj_get(business, "time_format");
8722 if (yyjson_is_str(time_format_value) && yyjson_get_str(time_format_value) != NULL && yyjson_get_str(time_format_value)[0] != '\0') {
8723 time_format = yyjson_get_str(time_format_value);
8724 }
8725 service_duration = native_json_obj_get_long_default(service, "duration_minutes", 45, NULL);
8726 if (service_duration <= 0) {
8727 service_duration = 45;
8728 }
8729
8730 if (error_message != NULL && error_message[0] != '\0') {
8731 TextBufferRuntime tmp = {0};
8732 if (!text_buffer_append_text_runtime(&tmp, "<p style=\"color:#9f1239;\">")
8733 || !text_buffer_append_html_escaped_runtime(&tmp, error_message)
8734 || !text_buffer_append_text_runtime(&tmp, "</p>")) {
8736 goto service_book_form_params_cleanup;
8737 }
8738 error_html = text_buffer_take_runtime(&tmp);
8739 }
8740
8741 booking_options = native_json_obj_get_array(service, "booking_options");
8742 {
8743 yyjson_arr_iter iter;
8744 yyjson_val *row = NULL;
8745 char *label = NULL;
8746 char *cost = NULL;
8747 long duration = service_duration;
8748 if (!text_buffer_append_text_runtime(&booking_option_options, "<option value=\"0\">")
8749 || !text_buffer_append_html_escaped_runtime(&booking_option_options, service_name != NULL ? service_name : "")
8750 || !text_buffer_append_text_runtime(&booking_option_options, "</option>")) {
8751 goto service_book_form_params_cleanup;
8752 }
8753 booking_option_count = 1;
8754 if (booking_options != NULL && yyjson_arr_iter_init(booking_options, &iter)) {
8755 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
8756 label = native_json_obj_get_string_dup(row, "label");
8757 cost = native_json_obj_get_string_dup(row, "cost");
8758 duration = native_json_obj_get_long_default(row, "duration_minutes", 45, NULL);
8759 if (duration <= 0) {
8760 duration = 45;
8761 }
8762 if ((label != NULL && label[0] != '\0') || duration > 0 || (cost != NULL && cost[0] != '\0')) {
8763 if (!text_buffer_append_format_runtime(&booking_option_options, "<option value=\"%ld\">", booking_option_count)
8764 || !text_buffer_append_html_escaped_runtime(&booking_option_options, label != NULL && label[0] != '\0' ? label : "Booking option")
8765 || !text_buffer_append_text_runtime(&booking_option_options, "</option>")) {
8766 free(label);
8767 free(cost);
8768 goto service_book_form_params_cleanup;
8769 }
8770 booking_option_count++;
8771 }
8772 free(label);
8773 free(cost);
8774 }
8775 }
8776 }
8777
8778 if (booking_option_count > 1) {
8779 if (!text_buffer_append_text_runtime(&booking_option_select, "<div class=\"form-group\"><label for=\"booking-option-index\" class=\"sr-only\">Booking option</label><select id=\"booking-option-index\" name=\"booking_option_index\" class=\"form-control\" required style=\"display:block;width:100%;min-height:2.75rem;appearance:none;-webkit-appearance:none;background-color:#fff;color:#111827;border:1px solid #d1d5db;border-radius:6px;padding:0.5rem 2.75rem 0.5rem 0.75rem;background-image:linear-gradient(45deg, transparent 50%, #374151 50%),linear-gradient(135deg, #374151 50%, transparent 50%);background-position:calc(100% - 18px) calc(50% - 3px),calc(100% - 12px) calc(50% - 3px);background-size:6px 6px,6px 6px;background-repeat:no-repeat;font-weight:500;\">")
8780 || !text_buffer_append_text_runtime(&booking_option_select, booking_option_options.text != NULL ? booking_option_options.text : "")
8781 || !text_buffer_append_text_runtime(&booking_option_select, "</select></div>")) {
8782 goto service_book_form_params_cleanup;
8783 }
8784 }
8785
8786 if (streq(time_format, "military")) {
8787 long hour = 0;
8788 time_grid_columns = "1fr 1fr";
8789 time_period_html = "";
8790 if (!text_buffer_append_text_runtime(&time_hour_options, "<option value=\"\">Hour</option>")) {
8791 goto service_book_form_params_cleanup;
8792 }
8793 for (hour = 0; hour <= 23; hour++) {
8794 if (!text_buffer_append_format_runtime(&time_hour_options, "<option value=\"%02ld\">%02ld</option>", hour, hour)) {
8795 goto service_book_form_params_cleanup;
8796 }
8797 }
8798 } else {
8799 long hour = 1;
8800 if (!text_buffer_append_text_runtime(&time_hour_options, "<option value=\"\">Hour</option>")) {
8801 goto service_book_form_params_cleanup;
8802 }
8803 for (hour = 1; hour <= 12; hour++) {
8804 if (!text_buffer_append_format_runtime(&time_hour_options, "<option value=\"%02ld\">%02ld</option>", hour, hour)) {
8805 goto service_book_form_params_cleanup;
8806 }
8807 }
8808 }
8809
8810 if (!text_buffer_append_text_runtime(&time_minute_options, "<option value=\"\">Minute</option>")) {
8811 goto service_book_form_params_cleanup;
8812 }
8813 {
8814 const int minute_values[] = {0, 10, 20, 30, 40, 50};
8815 size_t minute_index = 0;
8816 for (minute_index = 0; minute_index < (sizeof(minute_values) / sizeof(minute_values[0])); minute_index++) {
8817 if (!text_buffer_append_format_runtime(&time_minute_options, "<option value=\"%02d\">%02d</option>", minute_values[minute_index], minute_values[minute_index])) {
8818 goto service_book_form_params_cleanup;
8819 }
8820 }
8821 }
8822
8823 {
8824 size_t needed = strlen("/business//book//") + strlen(business_slug) + 32;
8825 save_path = (char *)malloc(needed);
8826 if (save_path == NULL) {
8827 goto service_book_form_params_cleanup;
8828 }
8829 snprintf(save_path, needed, "/business/%s/book/%ld/", business_slug, service_id);
8830 }
8831
8832 page_doc = yyjson_mut_doc_new(NULL);
8833 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
8834 if (page_doc == NULL || page_obj == NULL) {
8835 goto service_book_form_params_cleanup;
8836 }
8837 yyjson_mut_doc_set_root(page_doc, page_obj);
8838 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "service_name", service_name != NULL ? service_name : "");
8839 yyjson_mut_obj_add_int(page_doc, page_obj, "service_duration", service_duration);
8840 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "service_visual_html", service_visual_html != NULL ? service_visual_html : "");
8841 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "service_description", service_description != NULL ? service_description : "");
8842 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "error_html", error_html != NULL ? error_html : "");
8843 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "save_path", save_path != NULL ? save_path : "");
8844 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "booking_option_select_html", booking_option_select.text != NULL ? booking_option_select.text : "");
8845 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "time_grid_columns", time_grid_columns);
8846 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "time_hour_options_html", time_hour_options.text != NULL ? time_hour_options.text : "");
8847 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "time_minute_options_html", time_minute_options.text != NULL ? time_minute_options.text : "");
8848 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "time_period_html", time_period_html != NULL ? time_period_html : "");
8849 result = yyjson_mut_write(page_doc, YYJSON_WRITE_NOFLAG, NULL);
8850
8851service_book_form_params_cleanup:
8852 text_buffer_free_runtime(&booking_option_options);
8853 text_buffer_free_runtime(&booking_option_select);
8854 text_buffer_free_runtime(&time_hour_options);
8855 text_buffer_free_runtime(&time_minute_options);
8856 free(service_name);
8857 free(service_description);
8858 free(service_image_raw);
8859 free(service_image_url);
8860 free(service_visual_html);
8861 free(error_html);
8862 free(save_path);
8863 yyjson_mut_doc_free(page_doc);
8864 native_json_doc_free(state_doc);
8865 return result;
8866}
8867
8868static char *render_service_visual_html_dup_runtime(const char *app_root, const char *image_url, const char *service_name) {
8869 yyjson_mut_doc *doc = NULL;
8870 yyjson_mut_val *params = NULL;
8871 char *rendered = NULL;
8872 char *initials = NULL;
8873 if (app_root == NULL || app_root[0] == '\0') {
8874 return NULL;
8875 }
8876 doc = yyjson_mut_doc_new(NULL);
8877 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
8878 if (doc == NULL || params == NULL) {
8880 return NULL;
8881 }
8882 yyjson_mut_doc_set_root(doc, params);
8883 if (image_url != NULL && image_url[0] != '\0' && !streq(image_url, "null")) {
8884 yyjson_mut_obj_add_strcpy(doc, params, "image_url", image_url);
8885 yyjson_mut_obj_add_strcpy(doc, params, "image_alt", service_name != NULL ? service_name : "");
8886 rendered = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/service_visual_image.html", doc);
8887 } else {
8888 initials = service_abbreviation_dup_runtime(service_name);
8889 yyjson_mut_obj_add_strcpy(doc, params, "badge_color", service_badge_color_runtime(service_name));
8890 yyjson_mut_obj_add_strcpy(doc, params, "initials", initials != NULL ? initials : "XX");
8891 rendered = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/service_visual_badge.html", doc);
8892 }
8893 free(initials);
8895 return rendered;
8896}
8897
8898static char *render_service_sort_order_options_html_dup_runtime(yyjson_val *root, long business_id, long selected_order, int include_append, const char *app_root) {
8899 yyjson_val *services = state_array_runtime(root, "services");
8900 yyjson_arr_iter iter;
8901 yyjson_val *row = NULL;
8902 TextBufferRuntime buffer = {0};
8903 long service_count = 0;
8904 long max_order = 1;
8905 long current_order = 1;
8906 if (app_root == NULL || app_root[0] == '\0') {
8907 return NULL;
8908 }
8909 if (services != NULL && yyjson_arr_iter_init(services, &iter)) {
8910 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
8911 if (yyjson_is_obj(row) && value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
8912 service_count++;
8913 }
8914 }
8915 }
8916 max_order = include_append ? (service_count + 1) : service_count;
8917 if (max_order < 1) {
8918 max_order = 1;
8919 }
8920 if (selected_order <= 0 || selected_order > max_order) {
8921 selected_order = max_order;
8922 }
8923 for (current_order = 1; current_order <= max_order; current_order++) {
8925 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
8926 char *html = NULL;
8927 if (doc == NULL || params == NULL) {
8929 text_buffer_free_runtime(&buffer);
8930 return NULL;
8931 }
8932 yyjson_mut_doc_set_root(doc, params);
8933 yyjson_mut_obj_add_int(doc, params, "option_value", current_order);
8934 yyjson_mut_obj_add_strcpy(doc, params, "selected_attr", current_order == selected_order ? " selected" : "");
8935 html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/service_sort_order_option.html", doc);
8937 if (html == NULL || !text_buffer_append_text_runtime(&buffer, html)) {
8938 free(html);
8939 text_buffer_free_runtime(&buffer);
8940 return NULL;
8941 }
8942 free(html);
8943 }
8944 return text_buffer_take_runtime(&buffer);
8945}
8946
8947static char *render_service_booking_options_editor_html_dup_runtime(yyjson_val *root, long service_id, const char *app_root) {
8948 yyjson_val *service = service_id > 0 ? state_find_service_by_id_runtime(root, service_id) : NULL;
8949 yyjson_val *options = service != NULL ? native_json_obj_get_array(service, "booking_options") : NULL;
8950 yyjson_arr_iter iter;
8951 yyjson_val *row = NULL;
8952 TextBufferRuntime option_rows = {0};
8953 yyjson_mut_doc *doc = NULL;
8954 yyjson_mut_val *params = NULL;
8955 char *editor_html = NULL;
8956 long option_count = 0;
8957 if (app_root == NULL || app_root[0] == '\0') {
8958 return NULL;
8959 }
8960 if (options != NULL && yyjson_arr_iter_init(options, &iter)) {
8961 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
8962 yyjson_mut_doc *row_doc = yyjson_mut_doc_new(NULL);
8963 yyjson_mut_val *row_params = row_doc != NULL ? yyjson_mut_obj(row_doc) : NULL;
8964 char *row_html = NULL;
8965 char *label = NULL;
8966 char *cost = NULL;
8967 long duration = 45;
8968 option_count++;
8969 if (row_doc == NULL || row_params == NULL) {
8970 yyjson_mut_doc_free(row_doc);
8971 text_buffer_free_runtime(&option_rows);
8972 return NULL;
8973 }
8974 label = native_json_obj_get_string_dup(row, "label");
8975 cost = native_json_obj_get_string_dup(row, "cost");
8976 duration = native_json_obj_get_long_default(row, "duration_minutes", 45, NULL);
8977 if (duration <= 0) {
8978 duration = 45;
8979 }
8980 yyjson_mut_doc_set_root(row_doc, row_params);
8981 yyjson_mut_obj_add_int(row_doc, row_params, "option_index", option_count);
8982 yyjson_mut_obj_add_strcpy(row_doc, row_params, "label_value", label != NULL ? label : "");
8983 yyjson_mut_obj_add_int(row_doc, row_params, "duration_value", duration);
8984 yyjson_mut_obj_add_strcpy(row_doc, row_params, "cost_value", cost != NULL ? cost : "");
8985 row_html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/service_booking_option_row.html", row_doc);
8986 yyjson_mut_doc_free(row_doc);
8987 free(label);
8988 free(cost);
8989 if (row_html == NULL || !text_buffer_append_text_runtime(&option_rows, row_html)) {
8990 free(row_html);
8991 text_buffer_free_runtime(&option_rows);
8992 return NULL;
8993 }
8994 free(row_html);
8995 }
8996 }
8997 doc = yyjson_mut_doc_new(NULL);
8998 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
8999 if (doc == NULL || params == NULL) {
9001 text_buffer_free_runtime(&option_rows);
9002 return NULL;
9003 }
9004 yyjson_mut_doc_set_root(doc, params);
9005 yyjson_mut_obj_add_int(doc, params, "option_count", option_count);
9006 yyjson_mut_obj_add_strcpy(doc, params, "option_rows_html", option_rows.text != NULL ? option_rows.text : "");
9007 editor_html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/service_booking_options_editor.html", doc);
9009 text_buffer_free_runtime(&option_rows);
9010 return editor_html;
9011}
9012
9013static char *render_technician_checkboxes_html_dup_runtime(yyjson_val *root, long business_id, long service_id, const char *empty_message, int select_all, const char *app_root) {
9014 yyjson_val *technicians = state_array_runtime(root, "technicians");
9015 yyjson_val *service = service_id > 0 ? state_find_service_by_id_runtime(root, service_id) : NULL;
9016 yyjson_val *assigned = service != NULL ? native_json_obj_get_array(service, "technician_ids") : NULL;
9017 yyjson_arr_iter iter;
9018 yyjson_val *row = NULL;
9019 TextBufferRuntime buffer = {0};
9020 int emitted = 0;
9021 if (app_root == NULL || app_root[0] == '\0') {
9022 return NULL;
9023 }
9024 if (technicians != NULL && yyjson_arr_iter_init(technicians, &iter)) {
9025 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
9026 yyjson_mut_doc *doc = NULL;
9027 yyjson_mut_val *params = NULL;
9028 char *html = NULL;
9029 char *name = NULL;
9030 char *email = NULL;
9031 long technician_id = 0;
9032 int checked = 0;
9033 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
9034 continue;
9035 }
9036 technician_id = native_json_obj_get_long_default(row, "id", 0, NULL);
9037 checked = select_all || json_array_contains_long_runtime(assigned, technician_id);
9038 name = native_json_obj_get_string_dup(row, "name");
9039 email = native_json_obj_get_string_dup(row, "email");
9040 doc = yyjson_mut_doc_new(NULL);
9041 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
9042 if (doc == NULL || params == NULL) {
9043 free(name);
9044 free(email);
9046 text_buffer_free_runtime(&buffer);
9047 return NULL;
9048 }
9049 yyjson_mut_doc_set_root(doc, params);
9050 yyjson_mut_obj_add_int(doc, params, "technician_id", technician_id);
9051 yyjson_mut_obj_add_strcpy(doc, params, "checked_attr", checked ? " checked" : "");
9052 yyjson_mut_obj_add_strcpy(doc, params, "technician_name", name != NULL ? name : "");
9053 yyjson_mut_obj_add_strcpy(doc, params, "technician_email", email != NULL ? email : "");
9054 html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/technician_checkbox.html", doc);
9056 free(name);
9057 free(email);
9058 if (html == NULL || !text_buffer_append_text_runtime(&buffer, html)) {
9059 free(html);
9060 text_buffer_free_runtime(&buffer);
9061 return NULL;
9062 }
9063 free(html);
9064 emitted = 1;
9065 }
9066 }
9067 if (!emitted) {
9068 TextBufferRuntime empty = {0};
9069 if (!text_buffer_append_text_runtime(&empty, "<p class=\"muted\">") ||
9070 !text_buffer_append_html_escaped_runtime(&empty, empty_message != NULL ? empty_message : "") ||
9071 !text_buffer_append_text_runtime(&empty, "</p>")) {
9073 text_buffer_free_runtime(&buffer);
9074 return NULL;
9075 }
9076 return text_buffer_take_runtime(&empty);
9077 }
9078 return text_buffer_take_runtime(&buffer);
9079}
9080
9081static int parse_hhmm_minutes_runtime(const char *value, int *minutes_out) {
9082 int hour = 0;
9083 int minute = 0;
9084 if (minutes_out != NULL) {
9085 *minutes_out = 0;
9086 }
9087 if (value == NULL || sscanf(value, "%d:%d", &hour, &minute) != 2 || hour < 0 || hour > 23 || minute < 0 || minute > 59) {
9088 return 0;
9089 }
9090 if (minutes_out != NULL) {
9091 *minutes_out = (hour * 60) + minute;
9092 }
9093 return 1;
9094}
9095
9096static int parse_ymd_runtime(const char *date_value, int *year_out, int *month_out, int *day_out) {
9097 int year = 0;
9098 int month = 0;
9099 int day = 0;
9100 if (year_out != NULL) *year_out = 0;
9101 if (month_out != NULL) *month_out = 0;
9102 if (day_out != NULL) *day_out = 0;
9103 if (date_value == NULL || sscanf(date_value, "%d-%d-%d", &year, &month, &day) != 3 || month < 1 || month > 12 || day < 1 || day > 31) {
9104 return 0;
9105 }
9106 if (year_out != NULL) *year_out = year;
9107 if (month_out != NULL) *month_out = month;
9108 if (day_out != NULL) *day_out = day;
9109 return 1;
9110}
9111
9112static int time_ranges_overlap_runtime(int start_a_minutes, int duration_a, int start_b_minutes, int duration_b) {
9113 int end_a_minutes = start_a_minutes + duration_a;
9114 int end_b_minutes = start_b_minutes + duration_b;
9115 return start_a_minutes < end_b_minutes && start_b_minutes < end_a_minutes;
9116}
9117
9118static int business_open_for_slot_runtime(yyjson_val *root, long business_id, const char *date_value, const char *time_value, long duration_minutes) {
9119 yyjson_val *availability_rows = state_array_runtime(root, "business_availability");
9120 yyjson_arr_iter iter;
9121 yyjson_val *row = NULL;
9122 int year = 0;
9123 int month = 0;
9124 int day = 0;
9125 int slot_start = 0;
9126 int slot_end = 0;
9127 const char *day_name = NULL;
9128 if (root == NULL || business_id <= 0 || duration_minutes <= 0 || !parse_ymd_runtime(date_value, &year, &month, &day) || !parse_hhmm_minutes_runtime(time_value, &slot_start)) {
9129 return 0;
9130 }
9131 day_name = day_abbrev_for_ymd_runtime(year, month, day);
9132 slot_end = slot_start + (int)duration_minutes;
9133 if (availability_rows != NULL && yyjson_arr_iter_init(availability_rows, &iter)) {
9134 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
9135 char *row_day = NULL;
9136 char *start_text = NULL;
9137 char *end_text = NULL;
9138 int start_minutes = 0;
9139 int end_minutes = 0;
9140 int matches = 0;
9141 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
9142 continue;
9143 }
9144 row_day = native_json_obj_get_string_dup(row, "day_of_week");
9145 start_text = native_json_obj_get_string_dup(row, "start_time");
9146 end_text = native_json_obj_get_string_dup(row, "end_time");
9147 matches = row_day != NULL && streq(row_day, day_name) && parse_hhmm_minutes_runtime(start_text, &start_minutes) && parse_hhmm_minutes_runtime(end_text, &end_minutes) && slot_start >= start_minutes && slot_end <= end_minutes;
9148 free(row_day);
9149 free(start_text);
9150 free(end_text);
9151 if (matches) {
9152 return 1;
9153 }
9154 }
9155 }
9156 return 0;
9157}
9158
9159static int technician_booked_for_slot_runtime(yyjson_val *root, long technician_id, const char *date_value, const char *time_value, long duration_minutes) {
9160 yyjson_val *appointments = state_array_runtime(root, "appointments");
9161 yyjson_arr_iter iter;
9162 yyjson_val *row = NULL;
9163 int slot_start = 0;
9164 if (root == NULL || technician_id <= 0 || duration_minutes <= 0 || !parse_hhmm_minutes_runtime(time_value, &slot_start)) {
9165 return 0;
9166 }
9167 if (appointments != NULL && yyjson_arr_iter_init(appointments, &iter)) {
9168 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
9169 char *existing_date = NULL;
9170 char *existing_time = NULL;
9171 char *existing_status = NULL;
9172 long service_id = 0;
9173 yyjson_val *service = NULL;
9174 int existing_start = 0;
9175 int existing_duration = 45;
9176 int matches = 0;
9177 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "technician_id"), technician_id)) {
9178 continue;
9179 }
9180 existing_date = native_json_obj_get_string_dup(row, "date");
9181 existing_time = native_json_obj_get_string_dup(row, "time");
9182 existing_status = native_json_obj_get_string_dup(row, "status");
9183 if (existing_status != NULL && streq(existing_status, "cancelled")) {
9184 free(existing_date);
9185 free(existing_time);
9186 free(existing_status);
9187 continue;
9188 }
9189 service_id = native_json_obj_get_long_default(row, "service_id", 0, NULL);
9190 service = state_find_service_by_id_runtime(root, service_id);
9191 if (service != NULL) {
9192 existing_duration = (int)native_json_obj_get_long_default(service, "duration_minutes", 45, NULL);
9193 }
9194 matches = existing_date != NULL && streq(existing_date, date_value != NULL ? date_value : "") && parse_hhmm_minutes_runtime(existing_time, &existing_start) && time_ranges_overlap_runtime(slot_start, (int)duration_minutes, existing_start, existing_duration);
9195 free(existing_date);
9196 free(existing_time);
9197 free(existing_status);
9198 if (matches) {
9199 return 1;
9200 }
9201 }
9202 }
9203 return 0;
9204}
9205
9206static long assign_technician_for_slot_runtime(yyjson_val *root, long business_id, long service_id, const char *date_value, const char *time_value) {
9207 yyjson_val *service = state_find_service_by_id_runtime(root, service_id);
9208 yyjson_val *technician_ids = service != NULL ? native_json_obj_get_array(service, "technician_ids") : NULL;
9209 yyjson_val *availability_rows = state_array_runtime(root, "technician_availability");
9210 yyjson_arr_iter tech_iter;
9211 yyjson_val *tech_value = NULL;
9212 int year = 0;
9213 int month = 0;
9214 int day = 0;
9215 int slot_start = 0;
9216 int slot_end = 0;
9217 int duration_minutes = 45;
9218 const char *day_name = NULL;
9219 if (service != NULL) {
9220 duration_minutes = (int)native_json_obj_get_long_default(service, "duration_minutes", 45, NULL);
9221 }
9222 if (duration_minutes <= 0) {
9223 duration_minutes = 45;
9224 }
9225 if (root == NULL || business_id <= 0 || service_id <= 0 || technician_ids == NULL || !yyjson_is_arr(technician_ids) || !parse_ymd_runtime(date_value, &year, &month, &day) || !parse_hhmm_minutes_runtime(time_value, &slot_start)) {
9226 return 0;
9227 }
9228 day_name = day_abbrev_for_ymd_runtime(year, month, day);
9229 slot_end = slot_start + duration_minutes;
9230 if (yyjson_arr_iter_init(technician_ids, &tech_iter)) {
9231 while ((tech_value = yyjson_arr_iter_next(&tech_iter)) != NULL) {
9232 long technician_id = 0;
9233 yyjson_arr_iter avail_iter;
9234 yyjson_val *row = NULL;
9235 if (!long_from_value_runtime(tech_value, &technician_id) || technician_id <= 0) {
9236 continue;
9237 }
9238 if (availability_rows != NULL && yyjson_arr_iter_init(availability_rows, &avail_iter)) {
9239 while ((row = yyjson_arr_iter_next(&avail_iter)) != NULL) {
9240 char *row_day = NULL;
9241 char *start_text = NULL;
9242 char *end_text = NULL;
9243 int start_minutes = 0;
9244 int end_minutes = 0;
9245 int available = 0;
9246 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "technician_id"), technician_id)) {
9247 continue;
9248 }
9249 row_day = native_json_obj_get_string_dup(row, "day_of_week");
9250 start_text = native_json_obj_get_string_dup(row, "start_time");
9251 end_text = native_json_obj_get_string_dup(row, "end_time");
9252 available = row_day != NULL && streq(row_day, day_name) && parse_hhmm_minutes_runtime(start_text, &start_minutes) && parse_hhmm_minutes_runtime(end_text, &end_minutes) && slot_start >= start_minutes && slot_end <= end_minutes;
9253 free(row_day);
9254 free(start_text);
9255 free(end_text);
9256 if (available && !technician_booked_for_slot_runtime(root, technician_id, date_value, time_value, duration_minutes)) {
9257 return technician_id;
9258 }
9259 }
9260 }
9261 }
9262 }
9263 return 0;
9264}
9265
9266static int days_in_month_runtime(int year, int month) {
9267 switch (month) {
9268 case 1: case 3: case 5: case 7: case 8: case 10: case 12:
9269 return 31;
9270 case 4: case 6: case 9: case 11:
9271 return 30;
9272 case 2:
9273 return ((year % 400) == 0 || (((year % 4) == 0) && ((year % 100) != 0))) ? 29 : 28;
9274 default:
9275 return 30;
9276 }
9277}
9278
9279static const char *month_name_runtime(int month) {
9280 static const char *names[] = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" };
9281 if (month < 1 || month > 12) {
9282 return "Month";
9283 }
9284 return names[month - 1];
9285}
9286
9287static void month_shift_runtime(int year, int month, int shift, int *out_year, int *out_month) {
9288 int total = (year * 12) + (month - 1) + shift;
9289 if (out_year != NULL) {
9290 *out_year = total / 12;
9291 }
9292 if (out_month != NULL) {
9293 *out_month = (total % 12) + 1;
9294 }
9295}
9296
9297static int weekday_monday_index_runtime(int year, int month, int day) {
9298 struct tm value;
9299 time_t stamp;
9300 memset(&value, 0, sizeof(value));
9301 value.tm_year = year - 1900;
9302 value.tm_mon = month - 1;
9303 value.tm_mday = day;
9304 value.tm_hour = 12;
9305 stamp = mktime(&value);
9306 if (stamp == (time_t)-1) {
9307 return 1;
9308 }
9309 return value.tm_wday == 0 ? 7 : value.tm_wday;
9310}
9311
9312static const char *day_abbrev_for_ymd_runtime(int year, int month, int day) {
9313 static const char *days[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
9314 struct tm value;
9315 time_t stamp;
9316 memset(&value, 0, sizeof(value));
9317 value.tm_year = year - 1900;
9318 value.tm_mon = month - 1;
9319 value.tm_mday = day;
9320 value.tm_hour = 12;
9321 stamp = mktime(&value);
9322 if (stamp == (time_t)-1 || value.tm_wday < 0 || value.tm_wday > 6) {
9323 return "Mon";
9324 }
9325 return days[value.tm_wday];
9326}
9327
9328static int append_date_runtime(TextBufferRuntime *buffer, int year, int month, int day) {
9329 return text_buffer_append_format_runtime(buffer, "%04d-%02d-%02d", year, month, day);
9330}
9331
9332static char *render_business_dashboard_cards_html_dup_runtime(yyjson_val *root, yyjson_val *tenant_ids, const char *app_root) {
9333 yyjson_val *businesses = state_array_runtime(root, "businesses");
9334 yyjson_arr_iter iter;
9335 yyjson_val *row = NULL;
9336 TextBufferRuntime buffer = {0};
9337 if (businesses != NULL && yyjson_arr_iter_init(businesses, &iter)) {
9338 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
9339 yyjson_mut_doc *doc = NULL;
9340 yyjson_mut_val *params = NULL;
9341 char *name = NULL;
9342 char *description = NULL;
9343 char *slug = NULL;
9344 char *html = NULL;
9345 long business_id = 0;
9346 long tenant_id = 0;
9347 if (!yyjson_is_obj(row)) {
9348 continue;
9349 }
9350 business_id = native_json_obj_get_long_default(row, "id", 0, NULL);
9351 tenant_id = tenant_id_for_business_runtime(root, business_id);
9352 if (!json_array_contains_long_runtime(tenant_ids, tenant_id)) {
9353 continue;
9354 }
9355 name = native_json_obj_get_string_dup(row, "name");
9356 description = native_json_obj_get_string_dup(row, "description");
9357 slug = native_json_obj_get_string_dup(row, "slug");
9358 doc = yyjson_mut_doc_new(NULL);
9359 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
9360 if (doc == NULL || params == NULL) {
9361 free(name); free(description); free(slug); yyjson_mut_doc_free(doc); text_buffer_free_runtime(&buffer); return NULL;
9362 }
9363 yyjson_mut_doc_set_root(doc, params);
9364 yyjson_mut_obj_add_strcpy(doc, params, "business_name", name != NULL ? name : "");
9365 yyjson_mut_obj_add_strcpy(doc, params, "business_description", description != NULL ? description : "");
9366 yyjson_mut_obj_add_strcpy(doc, params, "business_slug", slug != NULL ? slug : "");
9367 html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/business_dashboard_card.html", doc);
9369 free(name); free(description); free(slug);
9370 if (html == NULL || !text_buffer_append_text_runtime(&buffer, html)) {
9371 free(html);
9372 text_buffer_free_runtime(&buffer);
9373 return NULL;
9374 }
9375 free(html);
9376 }
9377 }
9378 if (buffer.length == 0) {
9379 return strdup("<article class=\"card\"><p>No businesses yet.</p></article>");
9380 }
9381 return text_buffer_take_runtime(&buffer);
9382}
9383
9384static int emit_business_dashboard_page_json_runtime(yyjson_val *root, yyjson_val *tenant_ids, const char *app_root) {
9385 yyjson_mut_doc *doc = NULL;
9386 yyjson_mut_val *obj = NULL;
9387 char *cards_html = NULL;
9388 int code = 69;
9389 cards_html = render_business_dashboard_cards_html_dup_runtime(root, tenant_ids, app_root);
9390 if (cards_html == NULL) {
9391 return 69;
9392 }
9393 doc = yyjson_mut_doc_new(NULL);
9394 obj = doc != NULL ? yyjson_mut_obj(doc) : NULL;
9395 if (doc == NULL || obj == NULL) {
9396 free(cards_html);
9398 return 69;
9399 }
9400 yyjson_mut_doc_set_root(doc, obj);
9401 yyjson_mut_obj_add_strcpy(doc, obj, "business_cards_html", cards_html);
9403 free(cards_html);
9405 return code;
9406}
9407
9409 yyjson_val *root,
9410 const char *slug,
9411 const char *app_root,
9412 const char *invite_account_type_options_html
9413) {
9414 yyjson_val *business = state_find_business_by_slug_runtime(root, slug);
9415 yyjson_val *google_connection = NULL;
9416 yyjson_val *services = state_array_runtime(root, "services");
9417 yyjson_val *technicians = state_array_runtime(root, "technicians");
9418 yyjson_val *availability_rows = state_array_runtime(root, "business_availability");
9419 yyjson_val *appointments = state_array_runtime(root, "appointments");
9420 business_admin_service_list_runtime service_list = {0};
9421 yyjson_arr_iter iter;
9422 yyjson_val *row = NULL;
9423 TextBufferRuntime service_rows = {0};
9424 TextBufferRuntime technician_rows = {0};
9425 TextBufferRuntime availability_html = {0};
9426 yyjson_mut_doc *page_doc = NULL;
9427 yyjson_mut_val *page_obj = NULL;
9428 char *business_name = NULL;
9429 char *connection_status = NULL;
9430 char *public_state_button_html = NULL;
9431 char *new_service_sort_order_options_html = NULL;
9432 char *new_service_booking_options_editor_html = NULL;
9433 char *service_technician_checkboxes_html = NULL;
9434 char *availability_day_options_html = NULL;
9435 char *empty_html = NULL;
9436 long business_id = 0;
9437 long appointment_count = 0;
9438 long unassigned_count = 0;
9439 long technician_count = 0;
9440 int stripe_connected = 0;
9441 int is_public = 0;
9442 int code = 69;
9443 if (business == NULL || app_root == NULL || app_root[0] == '\0') {
9444 return 64;
9445 }
9446 business_id = native_json_obj_get_long_default(business, "id", 0, NULL);
9447 business_name = native_json_obj_get_string_dup(business, "name");
9448 stripe_connected = json_value_truthy_runtime(native_json_obj_get(business, "stripe_connected"));
9449 is_public = json_value_truthy_runtime(native_json_obj_get(business, "is_public"));
9450 google_connection = state_find_google_calendar_connection_for_business_runtime(root, business_id);
9451 connection_status = native_json_obj_get_string_dup(google_connection, "status");
9452 if (connection_status == NULL || connection_status[0] == '\0') {
9453 free(connection_status);
9454 connection_status = strdup("Disconnected");
9455 }
9456 {
9458 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
9459 if (doc == NULL || params == NULL) {
9460 goto business_admin_page_cleanup;
9461 }
9462 yyjson_mut_doc_set_root(doc, params);
9463 yyjson_mut_obj_add_strcpy(doc, params, "toggle_path", slug != NULL ? "" : "");
9464 yyjson_mut_obj_add_strcpy(doc, params, "background_color", is_public ? "#e8f5e9" : "#fdecea");
9465 yyjson_mut_obj_add_strcpy(doc, params, "text_color", is_public ? "#1b5e20" : "#9b2020");
9466 yyjson_mut_obj_add_strcpy(doc, params, "button_label", is_public ? "Public" : "Private");
9467 {
9468 char *toggle_path = NULL;
9469 size_t needed = strlen("/businesses//toggle-public/") + strlen(slug != NULL ? slug : "") + 1;
9470 toggle_path = (char *)malloc(needed);
9471 if (toggle_path == NULL) {
9473 goto business_admin_page_cleanup;
9474 }
9475 snprintf(toggle_path, needed, "/businesses/%s/toggle-public/", slug != NULL ? slug : "");
9476 yyjson_mut_obj_put(params, yyjson_mut_strcpy(doc, "toggle_path"), yyjson_mut_strcpy(doc, toggle_path));
9477 free(toggle_path);
9478 }
9479 public_state_button_html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/business_admin_public_state_button.html", doc);
9481 if (public_state_button_html == NULL) {
9482 goto business_admin_page_cleanup;
9483 }
9484 }
9485 if (!collect_business_service_list_runtime(services, business_id, &service_list)) {
9486 goto business_admin_page_cleanup;
9487 }
9488 new_service_sort_order_options_html = render_service_sort_order_options_html_dup_runtime(root, business_id, (long)service_list.count + 1, 1, app_root);
9489 new_service_booking_options_editor_html = render_service_booking_options_editor_html_dup_runtime(root, 0, app_root);
9490 service_technician_checkboxes_html = render_technician_checkboxes_html_dup_runtime(root, business_id, 0, "Create technicians first to assign them to services.", 1, app_root);
9491 availability_day_options_html = day_of_week_options_html_dup_runtime("Mon");
9492 if (new_service_sort_order_options_html == NULL || new_service_booking_options_editor_html == NULL || service_technician_checkboxes_html == NULL || availability_day_options_html == NULL) {
9493 goto business_admin_page_cleanup;
9494 }
9495 if (service_list.count > 0) {
9496 size_t index = 0;
9497 for (index = 0; index < service_list.count; index++) {
9498 yyjson_val *service = service_list.items[index].service;
9500 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
9501 yyjson_val *booking_options = native_json_obj_get_array(service, "booking_options");
9502 yyjson_val *technician_ids = native_json_obj_get_array(service, "technician_ids");
9503 yyjson_arr_iter option_iter;
9504 yyjson_val *option = NULL;
9505 yyjson_arr_iter tech_iter;
9506 yyjson_val *technician = NULL;
9507 char *service_name = native_json_obj_get_string_dup(service, "name");
9508 char *service_cost = native_json_obj_get_string_dup(service, "cost");
9509 char *service_description = native_json_obj_get_string_dup(service, "description");
9510 char *image_url_raw = native_json_obj_get_string_dup(service, "image_url");
9511 char *service_image_url = NULL;
9512 char *booking_summary = NULL;
9513 char *technician_names = NULL;
9514 char *image_html = NULL;
9515 char *description_html = NULL;
9516 char *booking_html = NULL;
9517 char *technicians_html = NULL;
9518 char *service_summary = NULL;
9519 char *fragment_html = NULL;
9520 long service_id = service_list.items[index].service_id;
9521 long duration_minutes = native_json_obj_get_long_default(service, "duration_minutes", 45, NULL);
9522 long order_index = (long)index + 1;
9523 int first_booking_option = 1;
9524 int first_technician = 1;
9525 if (doc == NULL || params == NULL) {
9527 free(service_name); free(service_cost); free(service_description); free(image_url_raw);
9528 goto business_admin_page_cleanup;
9529 }
9530 yyjson_mut_doc_set_root(doc, params);
9531 if (image_url_raw == NULL || image_url_raw[0] == '\0') {
9532 free(image_url_raw);
9533 image_url_raw = native_json_obj_get_string_dup(service, "image");
9534 }
9535 service_image_url = service_media_url_dup_runtime(slug, image_url_raw != NULL ? image_url_raw : "");
9536 if (service_image_url != NULL && service_image_url[0] != '\0') {
9537 TextBufferRuntime tmp = {0};
9538 if (!text_buffer_append_text_runtime(&tmp, "<div style=\"margin-bottom:0.75rem;\"><img src=\"") || !text_buffer_append_html_escaped_runtime(&tmp, service_image_url) || !text_buffer_append_text_runtime(&tmp, "\" alt=\"") || !text_buffer_append_html_escaped_runtime(&tmp, service_name != NULL ? service_name : "") || !text_buffer_append_text_runtime(&tmp, "\" style=\"width:100%;max-width:320px;max-height:180px;object-fit:cover;border-radius:12px;border:1px solid #d0d7de;display:block;\"></div>")) {
9541 free(service_name); free(service_cost); free(service_description); free(image_url_raw); free(service_image_url);
9542 goto business_admin_page_cleanup;
9543 }
9544 image_html = text_buffer_take_runtime(&tmp);
9545 }
9546 if (service_description != NULL && service_description[0] != '\0') {
9547 TextBufferRuntime tmp = {0};
9548 if (!text_buffer_append_text_runtime(&tmp, "<p style=\"margin-top:0.5rem;\">") || !text_buffer_append_html_escaped_runtime(&tmp, service_description) || !text_buffer_append_text_runtime(&tmp, "</p>")) {
9551 free(service_name); free(service_cost); free(service_description); free(image_url_raw); free(service_image_url); free(image_html);
9552 goto business_admin_page_cleanup;
9553 }
9554 description_html = text_buffer_take_runtime(&tmp);
9555 }
9556 if (booking_options != NULL && yyjson_arr_iter_init(booking_options, &option_iter)) {
9557 while ((option = yyjson_arr_iter_next(&option_iter)) != NULL) {
9558 char *label = native_json_obj_get_string_dup(option, "label");
9559 char *cost = native_json_obj_get_string_dup(option, "cost");
9560 long option_duration = native_json_obj_get_long_default(option, "duration_minutes", 45, NULL);
9561 char fallback_label[64];
9562 const char *label_text = label;
9563 if (label_text == NULL || label_text[0] == '\0') {
9564 snprintf(fallback_label, sizeof(fallback_label), "%ld min", option_duration);
9565 label_text = fallback_label;
9566 }
9567 {
9568 size_t current_len = booking_summary != NULL ? strlen(booking_summary) : 0;
9569 const char *cost_text = cost != NULL ? cost : "";
9570 size_t needed = current_len + (first_booking_option ? 0 : 2) + strlen(label_text) + 2 + strlen(cost_text) + 1;
9571 char *next = (char *)realloc(booking_summary, needed);
9572 if (next == NULL) {
9573 free(label); free(cost); yyjson_mut_doc_free(doc); free(service_name); free(service_cost); free(service_description); free(image_url_raw); free(service_image_url); free(image_html); free(description_html); free(booking_summary);
9574 goto business_admin_page_cleanup;
9575 }
9576 booking_summary = next;
9577 if (first_booking_option) {
9578 booking_summary[0] = '\0';
9579 } else {
9580 strcat(booking_summary, ", ");
9581 }
9582 strcat(booking_summary, label_text);
9583 strcat(booking_summary, " $");
9584 strcat(booking_summary, cost_text);
9585 first_booking_option = 0;
9586 }
9587 free(label);
9588 free(cost);
9589 }
9590 }
9591 if (booking_summary != NULL && booking_summary[0] != '\0') {
9592 TextBufferRuntime tmp = {0};
9593 if (!text_buffer_append_text_runtime(&tmp, "<p class=\"muted\" style=\"margin-top:0.5rem;\">Additional options: ") || !text_buffer_append_html_escaped_runtime(&tmp, booking_summary) || !text_buffer_append_text_runtime(&tmp, "</p>")) {
9596 free(service_name); free(service_cost); free(service_description); free(image_url_raw); free(service_image_url); free(image_html); free(description_html); free(booking_summary);
9597 goto business_admin_page_cleanup;
9598 }
9599 booking_html = text_buffer_take_runtime(&tmp);
9600 }
9601 if (technicians != NULL && yyjson_arr_iter_init(technicians, &tech_iter)) {
9602 while ((technician = yyjson_arr_iter_next(&tech_iter)) != NULL) {
9603 char *technician_name = NULL;
9604 long technician_id = 0;
9605 int selected = 0;
9606 yyjson_arr_iter assigned_iter;
9607 yyjson_val *assigned_item = NULL;
9608 if (!yyjson_is_obj(technician) || !value_long_equals_runtime(native_json_obj_get(technician, "business_id"), business_id)) {
9609 continue;
9610 }
9611 technician_id = native_json_obj_get_long_default(technician, "id", 0, NULL);
9612 if (technician_ids != NULL && yyjson_arr_iter_init(technician_ids, &assigned_iter)) {
9613 while ((assigned_item = yyjson_arr_iter_next(&assigned_iter)) != NULL) {
9614 long assigned_id = 0;
9615 if (long_from_value_runtime(assigned_item, &assigned_id) && assigned_id == technician_id) {
9616 selected = 1;
9617 break;
9618 }
9619 }
9620 } else {
9621 selected = 1;
9622 }
9623 if (!selected) {
9624 continue;
9625 }
9626 technician_name = native_json_obj_get_string_dup(technician, "name");
9627 if (technician_name != NULL && technician_name[0] != '\0') {
9628 size_t current_len = technician_names != NULL ? strlen(technician_names) : 0;
9629 size_t needed = current_len + (first_technician ? 0 : 2) + strlen(technician_name) + 1;
9630 char *next = (char *)realloc(technician_names, needed);
9631 if (next == NULL) {
9632 free(technician_name); yyjson_mut_doc_free(doc); free(service_name); free(service_cost); free(service_description); free(image_url_raw); free(service_image_url); free(image_html); free(description_html); free(booking_summary); free(booking_html); free(technician_names);
9633 goto business_admin_page_cleanup;
9634 }
9635 technician_names = next;
9636 if (first_technician) technician_names[0] = '\0'; else strcat(technician_names, ", ");
9637 strcat(technician_names, technician_name);
9638 first_technician = 0;
9639 }
9640 free(technician_name);
9641 }
9642 }
9643 if (technician_names != NULL && technician_names[0] != '\0') {
9644 TextBufferRuntime tmp = {0};
9645 if (!text_buffer_append_text_runtime(&tmp, "<p class=\"muted\" style=\"margin-top:0.5rem;\">Technicians: ") || !text_buffer_append_html_escaped_runtime(&tmp, technician_names) || !text_buffer_append_text_runtime(&tmp, "</p>")) {
9646 text_buffer_free_runtime(&tmp); yyjson_mut_doc_free(doc); free(service_name); free(service_cost); free(service_description); free(image_url_raw); free(service_image_url); free(image_html); free(description_html); free(booking_summary); free(booking_html); free(technician_names); goto business_admin_page_cleanup;
9647 }
9648 technicians_html = text_buffer_take_runtime(&tmp);
9649 } else {
9650 technicians_html = strdup("<p class=\"muted\" style=\"margin-top:0.5rem;\">Technicians: none assigned</p>");
9651 }
9652 if (duration_minutes <= 0) duration_minutes = 45;
9653 {
9654 TextBufferRuntime tmp = {0};
9655 if (!text_buffer_append_format_runtime(&tmp, "Order %ld · $%s · %ld min", order_index, service_cost != NULL ? service_cost : "", duration_minutes)) {
9656 text_buffer_free_runtime(&tmp); yyjson_mut_doc_free(doc); free(service_name); free(service_cost); free(service_description); free(image_url_raw); free(service_image_url); free(image_html); free(description_html); free(booking_summary); free(booking_html); free(technician_names); free(technicians_html); goto business_admin_page_cleanup;
9657 }
9658 service_summary = text_buffer_take_runtime(&tmp);
9659 }
9660 yyjson_mut_obj_add_strcpy(doc, params, "image_html", image_html != NULL ? image_html : "");
9661 yyjson_mut_obj_add_strcpy(doc, params, "service_name", service_name != NULL ? service_name : "");
9662 {
9663 char text[512];
9664 snprintf(text, sizeof(text), "/businesses/%s/services/%ld/move-up/", slug != NULL ? slug : "", service_id);
9665 yyjson_mut_obj_add_strcpy(doc, params, "move_up_path", text);
9666 snprintf(text, sizeof(text), "/businesses/%s/services/%ld/move-down/", slug != NULL ? slug : "", service_id);
9667 yyjson_mut_obj_add_strcpy(doc, params, "move_down_path", text);
9668 snprintf(text, sizeof(text), "/businesses/%s/services/%ld/edit/", slug != NULL ? slug : "", service_id);
9669 yyjson_mut_obj_add_strcpy(doc, params, "edit_path", text);
9670 snprintf(text, sizeof(text), "/businesses/%s/services/%ld/delete/", slug != NULL ? slug : "", service_id);
9671 yyjson_mut_obj_add_strcpy(doc, params, "delete_path", text);
9672 }
9673 yyjson_mut_obj_add_strcpy(doc, params, "move_up_disabled_attr", index == 0 ? " disabled" : "");
9674 yyjson_mut_obj_add_strcpy(doc, params, "move_down_disabled_attr", (index + 1) >= service_list.count ? " disabled" : "");
9675 yyjson_mut_obj_add_strcpy(doc, params, "service_summary", service_summary != NULL ? service_summary : "");
9676 yyjson_mut_obj_add_strcpy(doc, params, "service_description_html", description_html != NULL ? description_html : "");
9677 yyjson_mut_obj_add_strcpy(doc, params, "booking_options_html", booking_html != NULL ? booking_html : "");
9678 yyjson_mut_obj_add_strcpy(doc, params, "technicians_html", technicians_html != NULL ? technicians_html : "");
9679 fragment_html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/business_admin_service_card.html", doc);
9681 free(service_name); free(service_cost); free(service_description); free(image_url_raw); free(service_image_url); free(image_html); free(description_html); free(booking_summary); free(booking_html); free(technician_names); free(technicians_html); free(service_summary);
9682 if (fragment_html == NULL || !text_buffer_append_text_runtime(&service_rows, fragment_html)) {
9683 free(fragment_html);
9684 goto business_admin_page_cleanup;
9685 }
9686 free(fragment_html);
9687 }
9688 }
9689 if (service_rows.length == 0) {
9690 empty_html = render_business_admin_empty_state_dup_runtime(app_root, "No services yet.");
9691 if (empty_html == NULL || !text_buffer_append_text_runtime(&service_rows, empty_html)) {
9692 free(empty_html);
9693 goto business_admin_page_cleanup;
9694 }
9695 free(empty_html);
9696 empty_html = NULL;
9697 }
9698 if (technicians != NULL && yyjson_arr_iter_init(technicians, &iter)) {
9699 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
9700 yyjson_mut_doc *doc = NULL;
9701 yyjson_mut_val *params = NULL;
9702 char *name = NULL;
9703 char *email = NULL;
9704 char *availability_summary = NULL;
9705 char *fragment_html = NULL;
9706 char *day_options_html = NULL;
9707 long technician_id = 0;
9708 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) continue;
9709 technician_count++;
9710 technician_id = native_json_obj_get_long_default(row, "id", 0, NULL);
9711 name = native_json_obj_get_string_dup(row, "name");
9712 email = native_json_obj_get_string_dup(row, "email");
9713 if (availability_rows != NULL) {
9714 yyjson_arr_iter aiter;
9715 yyjson_val *availability = NULL;
9716 int first_entry = 1;
9717 if (yyjson_arr_iter_init(availability_rows, &aiter)) {
9718 while ((availability = yyjson_arr_iter_next(&aiter)) != NULL) {
9719 char *day = NULL; char *start = NULL; char *end = NULL; char *entry = NULL;
9720 size_t current_len = availability_summary != NULL ? strlen(availability_summary) : 0;
9721 size_t needed = 0;
9722 if (!yyjson_is_obj(availability) || !value_long_equals_runtime(native_json_obj_get(availability, "technician_id"), technician_id)) continue;
9723 day = native_json_obj_get_string_dup(availability, "day_of_week");
9724 start = native_json_obj_get_string_dup(availability, "start_time");
9725 end = native_json_obj_get_string_dup(availability, "end_time");
9726 if (day != NULL && start != NULL && end != NULL) {
9727 needed = strlen(day) + strlen(start) + strlen(end) + 4;
9728 entry = (char *)malloc(needed);
9729 if (entry != NULL) snprintf(entry, needed, "%s %s–%s", day, start, end);
9730 }
9731 if (entry != NULL) {
9732 size_t total = current_len + (first_entry ? 0 : 4) + strlen(entry) + 1;
9733 char *next = (char *)realloc(availability_summary, total);
9734 if (next == NULL) { free(day); free(start); free(end); free(entry); free(name); free(email); free(availability_summary); goto business_admin_page_cleanup; }
9735 availability_summary = next;
9736 if (first_entry) availability_summary[0] = '\0'; else strcat(availability_summary, "<br>");
9737 strcat(availability_summary, entry);
9738 first_entry = 0;
9739 }
9740 free(day); free(start); free(end); free(entry);
9741 }
9742 }
9743 }
9744 day_options_html = day_of_week_options_html_dup_runtime("Mon");
9745 doc = yyjson_mut_doc_new(NULL);
9746 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
9747 if (doc == NULL || params == NULL) { yyjson_mut_doc_free(doc); free(name); free(email); free(availability_summary); free(day_options_html); goto business_admin_page_cleanup; }
9748 yyjson_mut_doc_set_root(doc, params);
9749 yyjson_mut_obj_add_strcpy(doc, params, "technician_name", name != NULL ? name : "");
9750 yyjson_mut_obj_add_strcpy(doc, params, "technician_email", email != NULL ? email : "");
9751 yyjson_mut_obj_add_strcpy(doc, params, "technician_availability_summary_html", availability_summary != NULL && availability_summary[0] != '\0' ? availability_summary : "none set");
9752 yyjson_mut_obj_add_strcpy(doc, params, "availability_day_options_html", day_options_html != NULL ? day_options_html : "");
9753 {
9754 char text[512];
9755 snprintf(text, sizeof(text), "/businesses/%s/technicians/%ld/edit/", slug != NULL ? slug : "", technician_id);
9756 yyjson_mut_obj_add_strcpy(doc, params, "edit_path", text);
9757 snprintf(text, sizeof(text), "/businesses/%s/technicians/%ld/delete/", slug != NULL ? slug : "", technician_id);
9758 yyjson_mut_obj_add_strcpy(doc, params, "delete_path", text);
9759 snprintf(text, sizeof(text), "/businesses/technicians/%ld/availability/new/", technician_id);
9760 yyjson_mut_obj_add_strcpy(doc, params, "availability_create_path", text);
9761 }
9762 fragment_html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/business_admin_technician_card.html", doc);
9764 free(name); free(email); free(availability_summary); free(day_options_html);
9765 if (fragment_html == NULL || !text_buffer_append_text_runtime(&technician_rows, fragment_html)) {
9766 free(fragment_html);
9767 goto business_admin_page_cleanup;
9768 }
9769 free(fragment_html);
9770 }
9771 }
9772 if (technician_rows.length == 0) {
9773 empty_html = render_business_admin_empty_state_dup_runtime(app_root, "No technicians yet.");
9774 if (empty_html == NULL || !text_buffer_append_text_runtime(&technician_rows, empty_html)) { free(empty_html); goto business_admin_page_cleanup; }
9775 free(empty_html);
9776 empty_html = NULL;
9777 }
9778 if (availability_rows != NULL && yyjson_arr_iter_init(availability_rows, &iter)) {
9779 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
9780 yyjson_mut_doc *doc = NULL;
9781 yyjson_mut_val *params = NULL;
9782 char *day = NULL;
9783 char *start = NULL;
9784 char *end = NULL;
9785 char *time_range = NULL;
9786 char *fragment_html = NULL;
9787 long availability_id = 0;
9788 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) continue;
9789 day = native_json_obj_get_string_dup(row, "day_of_week");
9790 start = native_json_obj_get_string_dup(row, "start_time");
9791 end = native_json_obj_get_string_dup(row, "end_time");
9792 availability_id = native_json_obj_get_long_default(row, "id", 0, NULL);
9793 if (day != NULL && start != NULL && end != NULL) {
9794 size_t needed = strlen(start) + strlen(end) + 4;
9795 time_range = (char *)malloc(needed);
9796 if (time_range != NULL) snprintf(time_range, needed, "%s–%s", start, end);
9797 }
9798 doc = yyjson_mut_doc_new(NULL);
9799 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
9800 if (doc == NULL || params == NULL) { yyjson_mut_doc_free(doc); free(day); free(start); free(end); free(time_range); goto business_admin_page_cleanup; }
9801 yyjson_mut_doc_set_root(doc, params);
9802 yyjson_mut_obj_add_strcpy(doc, params, "day_of_week", day != NULL ? day : "");
9803 yyjson_mut_obj_add_strcpy(doc, params, "time_range", time_range != NULL ? time_range : "");
9804 {
9805 char text[512];
9806 snprintf(text, sizeof(text), "/businesses/%s/availability/%ld/delete/", slug != NULL ? slug : "", availability_id);
9807 yyjson_mut_obj_add_strcpy(doc, params, "delete_path", text);
9808 }
9809 fragment_html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/business_admin_availability_row.html", doc);
9811 free(day); free(start); free(end); free(time_range);
9812 if (fragment_html == NULL || !text_buffer_append_text_runtime(&availability_html, fragment_html)) { free(fragment_html); goto business_admin_page_cleanup; }
9813 free(fragment_html);
9814 }
9815 }
9816 if (availability_html.length == 0) {
9817 empty_html = render_business_admin_empty_state_dup_runtime(app_root, "No availability set.");
9818 if (empty_html == NULL || !text_buffer_append_text_runtime(&availability_html, empty_html)) { free(empty_html); goto business_admin_page_cleanup; }
9819 free(empty_html);
9820 empty_html = NULL;
9821 }
9822 if (appointments != NULL && yyjson_arr_iter_init(appointments, &iter)) {
9823 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
9824 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) continue;
9825 appointment_count++;
9826 if (native_json_obj_get_long_default(row, "technician_id", 0, NULL) == 0) {
9827 unassigned_count++;
9828 }
9829 }
9830 }
9831 page_doc = yyjson_mut_doc_new(NULL);
9832 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
9833 if (page_doc == NULL || page_obj == NULL) {
9834 goto business_admin_page_cleanup;
9835 }
9836 yyjson_mut_doc_set_root(page_doc, page_obj);
9837 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "business_name", business_name != NULL ? business_name : "");
9838 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "business_slug", slug != NULL ? slug : "");
9839 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "landing_path", slug != NULL ? "" : "");
9840 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "edit_path", slug != NULL ? "" : "");
9841 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "public_state_button_html", public_state_button_html != NULL ? public_state_button_html : "");
9842 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "calendar_path", slug != NULL ? "" : "");
9843 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "delete_path", slug != NULL ? "" : "");
9844 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "public_url_path", slug != NULL ? "" : "");
9845 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "stripe_connected", stripe_connected ? "true" : "false");
9846 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "connect_stripe_path", slug != NULL ? "" : "");
9847 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "connection_status", connection_status != NULL ? connection_status : "Disconnected");
9848 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "manage_path", slug != NULL ? "" : "");
9849 yyjson_mut_obj_add_int(page_doc, page_obj, "appointment_count", appointment_count);
9850 yyjson_mut_obj_add_int(page_doc, page_obj, "unassigned_count", unassigned_count);
9851 yyjson_mut_obj_add_int(page_doc, page_obj, "technician_count", technician_count);
9852 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "invite_create_path", slug != NULL ? "" : "");
9853 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "invite_account_type_options_html", invite_account_type_options_html != NULL ? invite_account_type_options_html : "");
9854 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "services_manage_path", slug != NULL ? "" : "");
9855 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "service_rows_html", service_rows.text != NULL ? service_rows.text : "");
9856 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "service_create_path", slug != NULL ? "" : "");
9857 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "new_service_sort_order_options_html", new_service_sort_order_options_html != NULL ? new_service_sort_order_options_html : "");
9858 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "new_service_booking_options_editor_html", new_service_booking_options_editor_html != NULL ? new_service_booking_options_editor_html : "");
9859 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "service_technician_checkboxes_html", service_technician_checkboxes_html != NULL ? service_technician_checkboxes_html : "");
9860 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "technicians_manage_path", slug != NULL ? "" : "");
9861 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "technician_rows_html", technician_rows.text != NULL ? technician_rows.text : "");
9862 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "technician_create_path", slug != NULL ? "" : "");
9863 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "availability_manage_path", slug != NULL ? "" : "");
9864 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "availability_rows_html", availability_html.text != NULL ? availability_html.text : "");
9865 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "availability_create_path", slug != NULL ? "" : "");
9866 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "availability_day_options_html", availability_day_options_html != NULL ? availability_day_options_html : "");
9867 {
9868 char path_text[512];
9869 snprintf(path_text, sizeof(path_text), "/b/%s/", slug != NULL ? slug : "");
9870 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "landing_path"), yyjson_mut_strcpy(page_doc, path_text));
9871 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "public_url_path"), yyjson_mut_strcpy(page_doc, path_text));
9872 snprintf(path_text, sizeof(path_text), "/businesses/%s/edit/", slug != NULL ? slug : "");
9873 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "edit_path"), yyjson_mut_strcpy(page_doc, path_text));
9874 snprintf(path_text, sizeof(path_text), "/business/%s/calendar/", slug != NULL ? slug : "");
9875 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "calendar_path"), yyjson_mut_strcpy(page_doc, path_text));
9876 snprintf(path_text, sizeof(path_text), "/businesses/%s/delete/", slug != NULL ? slug : "");
9877 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "delete_path"), yyjson_mut_strcpy(page_doc, path_text));
9878 snprintf(path_text, sizeof(path_text), "/businesses/%s/connect-stripe/", slug != NULL ? slug : "");
9879 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "connect_stripe_path"), yyjson_mut_strcpy(page_doc, path_text));
9880 snprintf(path_text, sizeof(path_text), "/businesses/%s/", slug != NULL ? slug : "");
9881 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "manage_path"), yyjson_mut_strcpy(page_doc, path_text));
9882 snprintf(path_text, sizeof(path_text), "/businesses/%s/invites/new/", slug != NULL ? slug : "");
9883 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "invite_create_path"), yyjson_mut_strcpy(page_doc, path_text));
9884 snprintf(path_text, sizeof(path_text), "/businesses/%s/services/", slug != NULL ? slug : "");
9885 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "services_manage_path"), yyjson_mut_strcpy(page_doc, path_text));
9886 snprintf(path_text, sizeof(path_text), "/businesses/%s/services/new/", slug != NULL ? slug : "");
9887 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "service_create_path"), yyjson_mut_strcpy(page_doc, path_text));
9888 snprintf(path_text, sizeof(path_text), "/businesses/%s/technicians/", slug != NULL ? slug : "");
9889 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "technicians_manage_path"), yyjson_mut_strcpy(page_doc, path_text));
9890 snprintf(path_text, sizeof(path_text), "/businesses/%s/technicians/new/", slug != NULL ? slug : "");
9891 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "technician_create_path"), yyjson_mut_strcpy(page_doc, path_text));
9892 snprintf(path_text, sizeof(path_text), "/businesses/%s/availability/", slug != NULL ? slug : "");
9893 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "availability_manage_path"), yyjson_mut_strcpy(page_doc, path_text));
9894 snprintf(path_text, sizeof(path_text), "/businesses/%s/availability/new/", slug != NULL ? slug : "");
9895 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "availability_create_path"), yyjson_mut_strcpy(page_doc, path_text));
9896 }
9897 code = emit_compact_json_mut_value_runtime(page_obj);
9898business_admin_page_cleanup:
9900 free(business_name);
9901 free(connection_status);
9902 free(public_state_button_html);
9903 free(new_service_sort_order_options_html);
9904 free(new_service_booking_options_editor_html);
9905 free(service_technician_checkboxes_html);
9906 free(availability_day_options_html);
9907 free(empty_html);
9908 text_buffer_free_runtime(&service_rows);
9909 text_buffer_free_runtime(&technician_rows);
9910 text_buffer_free_runtime(&availability_html);
9911 yyjson_mut_doc_free(page_doc);
9912 return code;
9913}
9914
9915/**
9916 * @brief Emits rendered explore-page template params using declarative business card fragments.
9917 * @param root Loaded runtime state root.
9918 * @param app_root UCAL app root used to locate fragment templates.
9919 * @return Process status code.
9920 */
9921static int emit_explore_page_json_runtime(yyjson_val *root, const char *app_root) {
9922 yyjson_val *businesses = state_array_runtime(root, "businesses");
9923 yyjson_arr_iter iter;
9924 yyjson_val *row = NULL;
9925 yyjson_mut_doc *page_doc = NULL;
9926 yyjson_mut_val *page_obj = NULL;
9927 TextBufferRuntime cards = {0};
9928 int code = 69;
9929 if (app_root == NULL || app_root[0] == '\0') {
9930 return 64;
9931 }
9932 if (businesses != NULL && yyjson_arr_iter_init(businesses, &iter)) {
9933 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
9934 yyjson_val *public_value = NULL;
9935 yyjson_mut_doc *doc = NULL;
9936 yyjson_mut_val *params = NULL;
9937 char *slug = NULL;
9938 char *name = NULL;
9939 char *description = NULL;
9940 char *raw_image_url = NULL;
9941 char *business_image_url = NULL;
9942 char *html = NULL;
9943 if (!yyjson_is_obj(row)) {
9944 continue;
9945 }
9946 public_value = native_json_obj_get(row, "is_public");
9947 if (!(yyjson_is_bool(public_value) && yyjson_get_bool(public_value))) {
9948 continue;
9949 }
9950 slug = native_json_obj_get_string_dup(row, "slug");
9951 if (slug == NULL || slug[0] == '\0') {
9952 free(slug);
9953 continue;
9954 }
9955 name = native_json_obj_get_string_dup(row, "name");
9956 description = native_json_obj_get_string_dup(row, "description");
9957 raw_image_url = native_json_obj_get_string_dup(row, "image_url");
9958 business_image_url = business_media_url_dup_runtime(slug, raw_image_url != NULL ? raw_image_url : "");
9959 doc = yyjson_mut_doc_new(NULL);
9960 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
9961 if (doc == NULL || params == NULL) {
9962 free(slug); free(name); free(description); free(raw_image_url); free(business_image_url);
9965 return 69;
9966 }
9967 yyjson_mut_doc_set_root(doc, params);
9968 yyjson_mut_obj_add_strcpy(doc, params, "business_slug", slug != NULL ? slug : "");
9969 yyjson_mut_obj_add_strcpy(doc, params, "business_name", name != NULL ? name : "");
9970 yyjson_mut_obj_add_strcpy(doc, params, "business_description", description != NULL ? description : "");
9971 yyjson_mut_obj_add_strcpy(doc, params, "business_image_url", business_image_url != NULL ? business_image_url : "");
9972 html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/explore/fragments/business_card.html", doc);
9974 free(slug); free(name); free(description); free(raw_image_url); free(business_image_url);
9975 if (html == NULL || !text_buffer_append_text_runtime(&cards, html)) {
9976 free(html);
9978 return 69;
9979 }
9980 free(html);
9981 }
9982 }
9983 if (cards.length == 0 && !text_buffer_append_text_runtime(&cards, "<article class=\"card\"><p>No businesses available yet.</p></article>")) {
9985 return 69;
9986 }
9987 page_doc = yyjson_mut_doc_new(NULL);
9988 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
9989 if (page_doc == NULL || page_obj == NULL
9990 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "business_cards_html", cards.text != NULL ? cards.text : "")) {
9992 yyjson_mut_doc_free(page_doc);
9993 return 69;
9994 }
9995 code = emit_compact_json_mut_value_runtime(page_obj);
9997 yyjson_mut_doc_free(page_doc);
9998 return code;
9999}
10000
10001/**
10002 * @brief Emits rendered profile-page template params using declarative subscription fragments.
10003 * @param root Loaded runtime state root.
10004 * @param user_id Signed-in user id.
10005 * @param app_root UCAL app root used to locate fragment templates.
10006 * @param payment_preference_options_html Pre-rendered select options HTML.
10007 * @return Process status code.
10008 */
10009static int emit_profile_render_page_json_runtime(yyjson_val *root, long user_id, const char *app_root, const char *payment_preference_options_html) {
10010 yyjson_val *user = state_find_user_by_id_runtime(root, user_id);
10011 yyjson_val *subscriptions = state_array_runtime(root, "subscriptions");
10012 yyjson_arr_iter iter;
10013 yyjson_val *row = NULL;
10014 yyjson_mut_doc *page_doc = NULL;
10015 yyjson_mut_val *page_obj = NULL;
10016 TextBufferRuntime subscription_rows = {0};
10017 char *subscriptions_block = NULL;
10018 char *full_name = NULL;
10019 char *email = NULL;
10020 char *phone_number = NULL;
10021 char *address = NULL;
10022 int code = 69;
10023 if (app_root == NULL || app_root[0] == '\0') {
10024 return 64;
10025 }
10026 if (subscriptions != NULL && yyjson_arr_iter_init(subscriptions, &iter)) {
10027 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
10028 long business_id = 0;
10029 yyjson_val *business = NULL;
10030 yyjson_mut_doc *doc = NULL;
10031 yyjson_mut_val *params = NULL;
10032 char *business_name = NULL;
10033 char *business_slug = NULL;
10034 char *html = NULL;
10035 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "user_id"), user_id)) {
10036 continue;
10037 }
10038 business_id = native_json_obj_get_long_default(row, "business_id", 0, NULL);
10039 business = state_find_business_by_id_runtime(root, business_id);
10040 business_name = business != NULL ? native_json_obj_get_string_dup(business, "name") : NULL;
10041 business_slug = business != NULL ? native_json_obj_get_string_dup(business, "slug") : NULL;
10042 doc = yyjson_mut_doc_new(NULL);
10043 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
10044 if (doc == NULL || params == NULL) {
10045 free(business_name); free(business_slug);
10047 text_buffer_free_runtime(&subscription_rows);
10048 return 69;
10049 }
10050 yyjson_mut_doc_set_root(doc, params);
10051 yyjson_mut_obj_add_strcpy(doc, params, "business_slug", business_slug != NULL ? business_slug : "");
10052 yyjson_mut_obj_add_strcpy(doc, params, "business_name", business_name != NULL ? business_name : "");
10053 html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/profile/fragments/subscription_row.html", doc);
10055 free(business_name); free(business_slug);
10056 if (html == NULL || !text_buffer_append_text_runtime(&subscription_rows, html)) {
10057 free(html);
10058 text_buffer_free_runtime(&subscription_rows);
10059 return 69;
10060 }
10061 free(html);
10062 }
10063 }
10064 if (subscription_rows.length > 0) {
10065 TextBufferRuntime block = {0};
10066 if (!text_buffer_append_text_runtime(&block, "<ul class=\"list-clean stack\">")
10067 || !text_buffer_append_text_runtime(&block, subscription_rows.text != NULL ? subscription_rows.text : "")
10068 || !text_buffer_append_text_runtime(&block, "</ul>")) {
10069 text_buffer_free_runtime(&subscription_rows);
10071 return 69;
10072 }
10073 subscriptions_block = text_buffer_take_runtime(&block);
10074 } else {
10076 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
10077 if (doc == NULL || params == NULL) {
10078 text_buffer_free_runtime(&subscription_rows);
10080 return 69;
10081 }
10082 yyjson_mut_doc_set_root(doc, params);
10083 subscriptions_block = render_business_fragment_from_doc_dup_runtime(app_root, "templates/profile/fragments/subscriptions_empty.html", doc);
10085 }
10086 full_name = user != NULL ? native_json_obj_get_string_dup(user, "full_name") : NULL;
10087 email = user != NULL ? native_json_obj_get_string_dup(user, "email") : NULL;
10088 phone_number = user != NULL ? native_json_obj_get_string_dup(user, "phone_number") : NULL;
10089 address = user != NULL ? native_json_obj_get_string_dup(user, "address") : NULL;
10090 page_doc = yyjson_mut_doc_new(NULL);
10091 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
10092 if (subscriptions_block == NULL || page_doc == NULL || page_obj == NULL
10093 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "full_name", full_name != NULL ? full_name : "")
10094 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "email", email != NULL ? email : "")
10095 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "phone_number", phone_number != NULL ? phone_number : "")
10096 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "address", address != NULL ? address : "")
10097 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "payment_preference_options_html", payment_preference_options_html != NULL ? payment_preference_options_html : "")
10098 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "google_status", "Disconnected")
10099 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "subscriptions_block", subscriptions_block)) {
10100 free(full_name); free(email); free(phone_number); free(address); free(subscriptions_block);
10101 text_buffer_free_runtime(&subscription_rows);
10102 yyjson_mut_doc_free(page_doc);
10103 return 69;
10104 }
10105 code = emit_compact_json_mut_value_runtime(page_obj);
10106 free(full_name); free(email); free(phone_number); free(address); free(subscriptions_block);
10107 text_buffer_free_runtime(&subscription_rows);
10108 yyjson_mut_doc_free(page_doc);
10109 return code;
10110}
10111
10112static int user_manages_business_runtime(yyjson_val *root, long user_id, long business_id) {
10113 yyjson_val *rows = NULL;
10114 yyjson_arr_iter iter;
10115 yyjson_val *row = NULL;
10116 long tenant_id = 0;
10117 if (root == NULL || user_id <= 0 || business_id <= 0) {
10118 return 0;
10119 }
10120 tenant_id = tenant_id_for_business_runtime(root, business_id);
10121 if (tenant_id <= 0) {
10122 return 0;
10123 }
10124 rows = state_array_runtime(root, "tenant_memberships");
10125 if (rows == NULL || !yyjson_arr_iter_init(rows, &iter)) {
10126 return 0;
10127 }
10128 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
10129 const char *membership_status = NULL;
10130 if (!yyjson_is_obj(row)) {
10131 continue;
10132 }
10133 if (native_json_obj_get_long_default(row, "user_id", 0, NULL) != user_id) {
10134 continue;
10135 }
10136 if (native_json_obj_get_long_default(row, "tenant_id", 0, NULL) != tenant_id) {
10137 continue;
10138 }
10139 membership_status = yyjson_get_str(native_json_obj_get(row, "membership_status"));
10140 if (membership_status != NULL && membership_status[0] != '\0' && !streq(membership_status, "active")) {
10141 continue;
10142 }
10143 return 1;
10144 }
10145 return 0;
10146}
10147
10149 const char *state_path,
10150 long user_id,
10151 const char *app_root,
10152 const char *payment_preference_options_html
10153) {
10154 yyjson_doc *state_doc = NULL;
10155 yyjson_val *root = NULL;
10156 yyjson_val *user = NULL;
10157 yyjson_val *subscriptions = NULL;
10158 yyjson_arr_iter iter;
10159 yyjson_val *row = NULL;
10160 yyjson_mut_doc *page_doc = NULL;
10161 yyjson_mut_val *page_obj = NULL;
10162 TextBufferRuntime subscription_rows = {0};
10163 char *subscriptions_block = NULL;
10164 char *full_name = NULL;
10165 char *email = NULL;
10166 char *phone_number = NULL;
10167 char *address = NULL;
10168 char *result = NULL;
10169
10170 if (state_path == NULL || state_path[0] == '\0' || app_root == NULL || app_root[0] == '\0') {
10171 return NULL;
10172 }
10173
10174 state_doc = native_json_doc_load_file(state_path);
10175 root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
10176 if (root == NULL) {
10177 native_json_doc_free(state_doc);
10178 return NULL;
10179 }
10180
10181 user = state_find_user_by_id_runtime(root, user_id);
10182 subscriptions = state_array_runtime(root, "subscriptions");
10183 if (subscriptions != NULL && yyjson_arr_iter_init(subscriptions, &iter)) {
10184 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
10185 long business_id = 0;
10186 yyjson_val *business = NULL;
10187 yyjson_mut_doc *doc = NULL;
10188 yyjson_mut_val *params = NULL;
10189 char *business_name = NULL;
10190 char *business_slug = NULL;
10191 char *html = NULL;
10192
10193 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "user_id"), user_id)) {
10194 continue;
10195 }
10196 business_id = native_json_obj_get_long_default(row, "business_id", 0, NULL);
10197 business = state_find_business_by_id_runtime(root, business_id);
10198 business_name = business != NULL ? native_json_obj_get_string_dup(business, "name") : NULL;
10199 business_slug = business != NULL ? native_json_obj_get_string_dup(business, "slug") : NULL;
10200 doc = yyjson_mut_doc_new(NULL);
10201 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
10202 if (doc == NULL || params == NULL) {
10203 free(business_name);
10204 free(business_slug);
10206 text_buffer_free_runtime(&subscription_rows);
10207 native_json_doc_free(state_doc);
10208 return NULL;
10209 }
10210 yyjson_mut_doc_set_root(doc, params);
10211 yyjson_mut_obj_add_strcpy(doc, params, "business_slug", business_slug != NULL ? business_slug : "");
10212 yyjson_mut_obj_add_strcpy(doc, params, "business_name", business_name != NULL ? business_name : "");
10213 html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/profile/fragments/subscription_row.html", doc);
10215 free(business_name);
10216 free(business_slug);
10217 if (html == NULL || !text_buffer_append_text_runtime(&subscription_rows, html)) {
10218 free(html);
10219 text_buffer_free_runtime(&subscription_rows);
10220 native_json_doc_free(state_doc);
10221 return NULL;
10222 }
10223 free(html);
10224 }
10225 }
10226
10227 if (subscription_rows.length > 0) {
10228 TextBufferRuntime block = {0};
10229 if (!text_buffer_append_text_runtime(&block, "<ul class=\"list-clean stack\">")
10230 || !text_buffer_append_text_runtime(&block, subscription_rows.text != NULL ? subscription_rows.text : "")
10231 || !text_buffer_append_text_runtime(&block, "</ul>")) {
10232 text_buffer_free_runtime(&subscription_rows);
10234 native_json_doc_free(state_doc);
10235 return NULL;
10236 }
10237 subscriptions_block = text_buffer_take_runtime(&block);
10238 } else {
10240 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
10241 if (doc == NULL || params == NULL) {
10242 text_buffer_free_runtime(&subscription_rows);
10244 native_json_doc_free(state_doc);
10245 return NULL;
10246 }
10247 yyjson_mut_doc_set_root(doc, params);
10248 subscriptions_block = render_business_fragment_from_doc_dup_runtime(app_root, "templates/profile/fragments/subscriptions_empty.html", doc);
10250 }
10251
10252 full_name = user != NULL ? native_json_obj_get_string_dup(user, "full_name") : NULL;
10253 email = user != NULL ? native_json_obj_get_string_dup(user, "email") : NULL;
10254 phone_number = user != NULL ? native_json_obj_get_string_dup(user, "phone_number") : NULL;
10255 address = user != NULL ? native_json_obj_get_string_dup(user, "address") : NULL;
10256 page_doc = yyjson_mut_doc_new(NULL);
10257 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
10258 if (subscriptions_block == NULL || page_doc == NULL || page_obj == NULL
10259 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "full_name", full_name != NULL ? full_name : "")
10260 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "email", email != NULL ? email : "")
10261 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "phone_number", phone_number != NULL ? phone_number : "")
10262 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "address", address != NULL ? address : "")
10263 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "payment_preference_options_html", payment_preference_options_html != NULL ? payment_preference_options_html : "")
10264 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "google_status", "Disconnected")
10265 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "subscriptions_block", subscriptions_block)) {
10266 free(full_name);
10267 free(email);
10268 free(phone_number);
10269 free(address);
10270 free(subscriptions_block);
10271 text_buffer_free_runtime(&subscription_rows);
10272 yyjson_mut_doc_free(page_doc);
10273 native_json_doc_free(state_doc);
10274 return NULL;
10275 }
10276
10277 yyjson_mut_doc_set_root(page_doc, page_obj);
10278 result = yyjson_mut_write(page_doc, YYJSON_WRITE_NOFLAG, NULL);
10279
10280 free(full_name);
10281 free(email);
10282 free(phone_number);
10283 free(address);
10284 free(subscriptions_block);
10285 text_buffer_free_runtime(&subscription_rows);
10286 yyjson_mut_doc_free(page_doc);
10287 native_json_doc_free(state_doc);
10288 return result;
10289}
10290
10291/**
10292 * @brief Emits rendered calendar-page template params using declarative booking and appointment fragments.
10293 * @param root Loaded runtime state root.
10294 * @param user_id Signed-in user id.
10295 * @param tenant_ids Tenant ids granting managed appointment actions.
10296 * @param app_root UCAL app root used to locate fragment templates.
10297 * @return Process status code.
10298 */
10299static int emit_calendar_page_json_runtime(yyjson_val *root, long user_id, yyjson_val *tenant_ids, const char *app_root) {
10300 yyjson_val *appointments = state_array_runtime(root, "appointments");
10301 yyjson_val *bookings = state_array_runtime(root, "bookings");
10302 yyjson_arr_iter iter;
10303 yyjson_val *row = NULL;
10304 yyjson_mut_doc *page_doc = NULL;
10305 yyjson_mut_val *page_obj = NULL;
10306 TextBufferRuntime booking_items = {0};
10307 TextBufferRuntime appointment_items = {0};
10308 int code = 69;
10309 int managed_mode = tenant_ids != NULL && yyjson_arr_size(tenant_ids) > 0;
10310 if (app_root == NULL || app_root[0] == '\0') {
10311 return 64;
10312 }
10313 if (appointments != NULL && yyjson_arr_iter_init(appointments, &iter)) {
10314 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
10315 yyjson_mut_doc *doc = NULL;
10316 yyjson_mut_val *params = NULL;
10317 yyjson_val *business = NULL;
10318 yyjson_val *service = NULL;
10319 long appointment_id = 0;
10320 long business_id = 0;
10321 long service_id = 0;
10322 long tenant_id = 0;
10323 char *business_name = NULL;
10324 char *service_name = NULL;
10325 char *date_value = NULL;
10326 char *time_value = NULL;
10327 char *status_value = NULL;
10328 char *fragment_path = NULL;
10329 char *html = NULL;
10330 if (!yyjson_is_obj(row)) {
10331 continue;
10332 }
10333 appointment_id = native_json_obj_get_long_default(row, "id", 0, NULL);
10334 business_id = native_json_obj_get_long_default(row, "business_id", 0, NULL);
10335 service_id = native_json_obj_get_long_default(row, "service_id", 0, NULL);
10336 if (managed_mode) {
10337 tenant_id = tenant_id_for_business_runtime(root, business_id);
10338 if (!json_array_contains_long_runtime(tenant_ids, tenant_id)) {
10339 continue;
10340 }
10341 } else if (!value_long_equals_runtime(native_json_obj_get(row, "user_id"), user_id)) {
10342 continue;
10343 }
10344 business = state_find_business_by_id_runtime(root, business_id);
10345 service = state_find_service_by_id_runtime(root, service_id);
10346 business_name = business != NULL ? native_json_obj_get_string_dup(business, "name") : NULL;
10347 service_name = service != NULL ? native_json_obj_get_string_dup(service, "name") : NULL;
10348 date_value = native_json_obj_get_string_dup(row, "date");
10349 time_value = native_json_obj_get_string_dup(row, "time");
10350 status_value = native_json_obj_get_string_dup(row, "status");
10351 doc = yyjson_mut_doc_new(NULL);
10352 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
10353 if (doc == NULL || params == NULL) {
10354 free(business_name); free(service_name); free(date_value); free(time_value); free(status_value);
10356 text_buffer_free_runtime(&booking_items); text_buffer_free_runtime(&appointment_items);
10357 return 69;
10358 }
10359 yyjson_mut_doc_set_root(doc, params);
10360 yyjson_mut_obj_add_strcpy(doc, params, "service_name", service_name != NULL ? service_name : "");
10361 yyjson_mut_obj_add_strcpy(doc, params, "business_name", business_name != NULL ? business_name : "");
10362 yyjson_mut_obj_add_strcpy(doc, params, "date_value", date_value != NULL ? date_value : "");
10363 yyjson_mut_obj_add_strcpy(doc, params, "time_value", time_value != NULL ? time_value : "");
10364 yyjson_mut_obj_add_strcpy(doc, params, "status_value", status_value != NULL ? status_value : "");
10365 if (managed_mode) {
10366 char complete_path[128];
10367 char cancel_path[128];
10368 snprintf(complete_path, sizeof(complete_path), "/appointments/%ld/complete/", appointment_id);
10369 snprintf(cancel_path, sizeof(cancel_path), "/appointments/%ld/cancel/", appointment_id);
10370 yyjson_mut_obj_add_strcpy(doc, params, "complete_path", complete_path);
10371 yyjson_mut_obj_add_strcpy(doc, params, "cancel_path", cancel_path);
10372 fragment_path = "templates/calendar/fragments/managed_appointment_row.html";
10373 } else {
10374 char request_cancel_path[128];
10375 snprintf(request_cancel_path, sizeof(request_cancel_path), "/appointments/%ld/request-cancel/", appointment_id);
10376 yyjson_mut_obj_add_strcpy(doc, params, "request_cancel_path", request_cancel_path);
10377 fragment_path = "templates/calendar/fragments/customer_appointment_row.html";
10378 }
10379 html = render_business_fragment_from_doc_dup_runtime(app_root, fragment_path, doc);
10381 free(business_name); free(service_name); free(date_value); free(time_value); free(status_value);
10382 if (html == NULL || !text_buffer_append_text_runtime(&appointment_items, html)) {
10383 free(html);
10384 text_buffer_free_runtime(&booking_items); text_buffer_free_runtime(&appointment_items);
10385 return 69;
10386 }
10387 free(html);
10388 }
10389 }
10390 if (bookings != NULL && yyjson_arr_iter_init(bookings, &iter)) {
10391 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
10392 yyjson_mut_doc *doc = NULL;
10393 yyjson_mut_val *params = NULL;
10394 char *booking_title = NULL;
10395 char *date_value = NULL;
10396 char *time_value = NULL;
10397 char *status_value = NULL;
10398 char *html = NULL;
10399 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "user_id"), user_id)) {
10400 continue;
10401 }
10402 booking_title = native_json_obj_get_string_dup(row, "title");
10403 date_value = native_json_obj_get_string_dup(row, "date");
10404 time_value = native_json_obj_get_string_dup(row, "time");
10405 status_value = native_json_obj_get_string_dup(row, "status");
10406 doc = yyjson_mut_doc_new(NULL);
10407 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
10408 if (doc == NULL || params == NULL) {
10409 free(booking_title); free(date_value); free(time_value); free(status_value);
10411 text_buffer_free_runtime(&booking_items); text_buffer_free_runtime(&appointment_items);
10412 return 69;
10413 }
10414 yyjson_mut_doc_set_root(doc, params);
10415 yyjson_mut_obj_add_strcpy(doc, params, "booking_title", booking_title != NULL ? booking_title : "");
10416 yyjson_mut_obj_add_strcpy(doc, params, "date_value", date_value != NULL ? date_value : "");
10417 yyjson_mut_obj_add_strcpy(doc, params, "time_value", time_value != NULL ? time_value : "");
10418 yyjson_mut_obj_add_strcpy(doc, params, "status_value", status_value != NULL ? status_value : "");
10419 html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/calendar/fragments/booking_row.html", doc);
10421 free(booking_title); free(date_value); free(time_value); free(status_value);
10422 if (html == NULL || !text_buffer_append_text_runtime(&booking_items, html)) {
10423 free(html);
10424 text_buffer_free_runtime(&booking_items); text_buffer_free_runtime(&appointment_items);
10425 return 69;
10426 }
10427 free(html);
10428 }
10429 }
10430 if (appointment_items.length == 0 && !text_buffer_append_text_runtime(&appointment_items, "<li class=\"card\"><p>No appointments yet.</p></li>")) {
10431 text_buffer_free_runtime(&booking_items); text_buffer_free_runtime(&appointment_items);
10432 return 69;
10433 }
10434 if (booking_items.length == 0 && !text_buffer_append_text_runtime(&booking_items, "<li class=\"card\"><p>No bookings yet.</p></li>")) {
10435 text_buffer_free_runtime(&booking_items); text_buffer_free_runtime(&appointment_items);
10436 return 69;
10437 }
10438 page_doc = yyjson_mut_doc_new(NULL);
10439 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
10440 if (page_doc == NULL || page_obj == NULL
10441 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "booking_items", booking_items.text != NULL ? booking_items.text : "")
10442 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "appointment_items", appointment_items.text != NULL ? appointment_items.text : "")) {
10443 text_buffer_free_runtime(&booking_items); text_buffer_free_runtime(&appointment_items);
10444 yyjson_mut_doc_free(page_doc);
10445 return 69;
10446 }
10447 code = emit_compact_json_mut_value_runtime(page_obj);
10448 text_buffer_free_runtime(&booking_items);
10449 text_buffer_free_runtime(&appointment_items);
10450 yyjson_mut_doc_free(page_doc);
10451 return code;
10452}
10453
10454static int emit_appointments_page_json_runtime(yyjson_val *root, long user_id, yyjson_val *tenant_ids, const char *app_root) {
10455 yyjson_val *appointments = state_array_runtime(root, "appointments");
10456 yyjson_arr_iter iter;
10457 yyjson_val *row = NULL;
10458 yyjson_mut_doc *page_doc = NULL;
10459 yyjson_mut_val *page_obj = NULL;
10460 TextBufferRuntime appointment_items = {0};
10461 const char *heading = "Appointments";
10462 const char *tips_block = "";
10463 int managed_mode = tenant_ids != NULL && yyjson_arr_size(tenant_ids) > 0;
10464 int code = 69;
10465 if (app_root == NULL || app_root[0] == '\0') {
10466 return 64;
10467 }
10468 if (managed_mode) {
10469 heading = "Manage Bookings";
10470 }
10471 if (appointments != NULL && yyjson_arr_iter_init(appointments, &iter)) {
10472 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
10473 yyjson_mut_doc *doc = NULL;
10474 yyjson_mut_val *params = NULL;
10475 yyjson_val *business = NULL;
10476 yyjson_val *service = NULL;
10477 yyjson_val *customer = NULL;
10478 yyjson_val *technician = NULL;
10479 long appointment_id = 0;
10480 long business_id = 0;
10481 long service_id = 0;
10482 long customer_id = 0;
10483 long technician_id = 0;
10484 long tenant_id = 0;
10485 char *business_name = NULL;
10486 char *service_name = NULL;
10487 char *customer_name = NULL;
10488 char *technician_name = NULL;
10489 char *date_value = NULL;
10490 char *time_value = NULL;
10491 char *status_value = NULL;
10492 char *technician_html = NULL;
10493 char *html = NULL;
10494 TextBufferRuntime tech = {0};
10495 const char *fragment_path = NULL;
10496 if (!yyjson_is_obj(row)) {
10497 continue;
10498 }
10499 appointment_id = native_json_obj_get_long_default(row, "id", 0, NULL);
10500 business_id = native_json_obj_get_long_default(row, "business_id", 0, NULL);
10501 service_id = native_json_obj_get_long_default(row, "service_id", 0, NULL);
10502 customer_id = native_json_obj_get_long_default(row, "user_id", 0, NULL);
10503 technician_id = native_json_obj_get_long_default(row, "technician_id", 0, NULL);
10504 if (managed_mode) {
10505 tenant_id = tenant_id_for_business_runtime(root, business_id);
10506 if (!json_array_contains_long_runtime(tenant_ids, tenant_id)) {
10507 continue;
10508 }
10509 } else if (customer_id != user_id) {
10510 continue;
10511 }
10512 business = state_find_business_by_id_runtime(root, business_id);
10513 service = state_find_service_by_id_runtime(root, service_id);
10514 customer = state_find_user_by_id_runtime(root, customer_id);
10515 technician = state_find_technician_for_business_runtime(root, technician_id, business_id);
10516 business_name = business != NULL ? native_json_obj_get_string_dup(business, "name") : NULL;
10517 service_name = service != NULL ? native_json_obj_get_string_dup(service, "name") : NULL;
10518 customer_name = customer != NULL ? native_json_obj_get_string_dup(customer, "full_name") : NULL;
10519 technician_name = technician != NULL ? native_json_obj_get_string_dup(technician, "name") : NULL;
10520 date_value = native_json_obj_get_string_dup(row, "date");
10521 time_value = native_json_obj_get_string_dup(row, "time");
10522 status_value = native_json_obj_get_string_dup(row, "status");
10523 if (technician_name != NULL && technician_name[0] != '\0') {
10524 if (managed_mode) {
10525 if (!text_buffer_append_text_runtime(&tech, "<br><span class=\"muted\">Technician: ")
10526 || !text_buffer_append_html_escaped_runtime(&tech, technician_name)
10527 || !text_buffer_append_text_runtime(&tech, "</span>")) {
10528 free(business_name); free(service_name); free(customer_name); free(technician_name); free(date_value); free(time_value); free(status_value);
10529 text_buffer_free_runtime(&tech); text_buffer_free_runtime(&appointment_items);
10530 return 69;
10531 }
10532 } else {
10533 if (!text_buffer_append_text_runtime(&tech, "<br>👤 Technician: ")
10534 || !text_buffer_append_html_escaped_runtime(&tech, technician_name)) {
10535 free(business_name); free(service_name); free(customer_name); free(technician_name); free(date_value); free(time_value); free(status_value);
10536 text_buffer_free_runtime(&tech); text_buffer_free_runtime(&appointment_items);
10537 return 69;
10538 }
10539 }
10540 technician_html = text_buffer_take_runtime(&tech);
10541 }
10542 doc = yyjson_mut_doc_new(NULL);
10543 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
10544 if (doc == NULL || params == NULL) {
10545 free(business_name); free(service_name); free(customer_name); free(technician_name); free(date_value); free(time_value); free(status_value); free(technician_html);
10546 yyjson_mut_doc_free(doc); text_buffer_free_runtime(&appointment_items);
10547 return 69;
10548 }
10549 yyjson_mut_doc_set_root(doc, params);
10550 yyjson_mut_obj_add_strcpy(doc, params, "service_name", service_name != NULL ? service_name : "");
10551 yyjson_mut_obj_add_strcpy(doc, params, "business_name", business_name != NULL ? business_name : "");
10552 yyjson_mut_obj_add_strcpy(doc, params, "date_value", date_value != NULL ? date_value : "");
10553 yyjson_mut_obj_add_strcpy(doc, params, "time_value", time_value != NULL ? time_value : "");
10554 yyjson_mut_obj_add_strcpy(doc, params, "status_value", status_value != NULL ? status_value : "");
10555 yyjson_mut_obj_add_strcpy(doc, params, "technician_html", technician_html != NULL ? technician_html : "");
10556 if (managed_mode) {
10557 char complete_path[128];
10558 char cancel_path[128];
10559 yyjson_mut_obj_add_strcpy(doc, params, "customer_name", customer_name != NULL ? customer_name : "");
10560 snprintf(complete_path, sizeof(complete_path), "/appointments/%ld/complete/", appointment_id);
10561 snprintf(cancel_path, sizeof(cancel_path), "/appointments/%ld/cancel/", appointment_id);
10562 yyjson_mut_obj_add_strcpy(doc, params, "complete_path", complete_path);
10563 yyjson_mut_obj_add_strcpy(doc, params, "cancel_path", cancel_path);
10564 fragment_path = "templates/appointments/fragments/managed_appointment_row.html";
10565 } else {
10566 char detail_path[128];
10567 char request_cancel_path[128];
10568 snprintf(detail_path, sizeof(detail_path), "/appointments/%ld/", appointment_id);
10569 snprintf(request_cancel_path, sizeof(request_cancel_path), "/appointments/%ld/request-cancel/", appointment_id);
10570 yyjson_mut_obj_add_strcpy(doc, params, "detail_path", detail_path);
10571 yyjson_mut_obj_add_strcpy(doc, params, "request_cancel_path", request_cancel_path);
10572 fragment_path = "templates/appointments/fragments/customer_appointment_row.html";
10573 }
10574 html = render_business_fragment_from_doc_dup_runtime(app_root, fragment_path, doc);
10576 free(business_name); free(service_name); free(customer_name); free(technician_name); free(date_value); free(time_value); free(status_value); free(technician_html);
10577 if (html == NULL || !text_buffer_append_text_runtime(&appointment_items, html)) {
10578 free(html);
10579 text_buffer_free_runtime(&appointment_items);
10580 return 69;
10581 }
10582 free(html);
10583 }
10584 }
10585 if (appointment_items.length == 0) {
10586 if (!text_buffer_append_text_runtime(&appointment_items, "<li><p>No appointments. <a href=\"/explore/\">Browse businesses</a> to book.</p></li>")) {
10587 text_buffer_free_runtime(&appointment_items);
10588 return 69;
10589 }
10590 }
10591 if (!managed_mode) {
10592 tips_block = "<div class=\"card\" style=\"margin-top:2rem;background:var(--gray-50);\"><h3>Tips</h3><ul style=\"margin:0.5rem 0;padding-left:1.5rem;\"><li>Use this page to review your active appointments and request cancellations</li><li>Contact the business owner directly using the contact details shown in appointment detail</li><li>If you need to cancel, do so as early as possible</li></ul></div>";
10593 }
10594 page_doc = yyjson_mut_doc_new(NULL);
10595 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
10596 if (page_doc == NULL || page_obj == NULL
10597 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "heading", heading)
10598 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "appointment_items", appointment_items.text != NULL ? appointment_items.text : "")
10599 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "tips_block", tips_block)) {
10600 text_buffer_free_runtime(&appointment_items);
10601 yyjson_mut_doc_free(page_doc);
10602 return 69;
10603 }
10604 code = emit_compact_json_mut_value_runtime(page_obj);
10605 text_buffer_free_runtime(&appointment_items);
10606 yyjson_mut_doc_free(page_doc);
10607 return code;
10608}
10609
10610static int emit_business_edit_page_json_runtime(yyjson_val *root, const char *slug) {
10611 yyjson_val *business = state_find_business_by_slug_runtime(root, slug);
10612 yyjson_mut_doc *doc = NULL;
10613 yyjson_mut_val *page = NULL;
10614 char *name = NULL;
10615 char *description = NULL;
10616 char *address = NULL;
10617 char *raw_image = NULL;
10618 char *image_url = NULL;
10619 char *current_logo_html = NULL;
10620 char *public_options_html = NULL;
10621 char *time_format_options_html = NULL;
10622 char *time_format = NULL;
10623 int is_public = 0;
10624 TextBufferRuntime buffer = {0};
10625 int code = 69;
10626 if (business == NULL || slug == NULL || slug[0] == '\0') {
10627 return 0;
10628 }
10629 name = native_json_obj_get_string_dup(business, "name");
10630 description = native_json_obj_get_string_dup(business, "description");
10631 address = native_json_obj_get_string_dup(business, "address");
10632 raw_image = native_json_obj_get_string_dup(business, "image_url");
10633 image_url = business_media_url_dup_runtime(slug, raw_image != NULL ? raw_image : "");
10634 time_format = native_json_obj_get_string_dup(business, "time_format");
10635 is_public = json_value_truthy_runtime(native_json_obj_get(business, "is_public"));
10636 if (image_url != NULL && image_url[0] != '\0') {
10637 if (!text_buffer_append_text_runtime(&buffer, "<img src=\"")
10638 || !text_buffer_append_html_escaped_runtime(&buffer, image_url)
10639 || !text_buffer_append_text_runtime(&buffer, "\" alt=\"")
10640 || !text_buffer_append_html_escaped_runtime(&buffer, name != NULL ? name : "")
10641 || !text_buffer_append_text_runtime(&buffer, "\" style=\"max-width:180px;max-height:180px;border-radius:12px;border:1px solid #d0d7de;display:block;\">")) {
10642 goto business_edit_cleanup;
10643 }
10644 } else if (!text_buffer_append_text_runtime(&buffer, "<p class=\"muted\" style=\"margin:0;\">No logo uploaded.</p>")) {
10645 goto business_edit_cleanup;
10646 }
10647 current_logo_html = text_buffer_take_runtime(&buffer);
10648 if (current_logo_html == NULL) {
10649 goto business_edit_cleanup;
10650 }
10651 public_options_html = strdup(is_public ? "<option value=\"true\" selected>Public</option><option value=\"false\">Private</option>" : "<option value=\"true\">Public</option><option value=\"false\" selected>Private</option>");
10652 time_format_options_html = strdup(time_format != NULL && streq(time_format, "military") ? "<option value=\"am_pm\">12-hour (AM/PM)</option><option value=\"military\" selected>24-hour (Military)</option>" : "<option value=\"am_pm\" selected>12-hour (AM/PM)</option><option value=\"military\">24-hour (Military)</option>");
10653 doc = yyjson_mut_doc_new(NULL);
10654 page = doc != NULL ? yyjson_mut_obj(doc) : NULL;
10655 if (public_options_html == NULL || time_format_options_html == NULL || doc == NULL || page == NULL) {
10656 goto business_edit_cleanup;
10657 }
10658 yyjson_mut_doc_set_root(doc, page);
10659 {
10660 char save_path[256];
10661 char cancel_path[256];
10662 snprintf(save_path, sizeof(save_path), "/businesses/%s/edit/", slug);
10663 snprintf(cancel_path, sizeof(cancel_path), "/businesses/%s/", slug);
10664 if (!yyjson_mut_obj_add_strcpy(doc, page, "save_path", save_path)
10665 || !yyjson_mut_obj_add_strcpy(doc, page, "cancel_path", cancel_path)
10666 || !yyjson_mut_obj_add_strcpy(doc, page, "current_logo_html", current_logo_html != NULL ? current_logo_html : "")
10667 || !yyjson_mut_obj_add_strcpy(doc, page, "business_name", name != NULL ? name : "")
10668 || !yyjson_mut_obj_add_strcpy(doc, page, "business_description", description != NULL ? description : "")
10669 || !yyjson_mut_obj_add_strcpy(doc, page, "business_address", address != NULL ? address : "")
10670 || !yyjson_mut_obj_add_strcpy(doc, page, "public_options_html", public_options_html != NULL ? public_options_html : "")
10671 || !yyjson_mut_obj_add_strcpy(doc, page, "time_format_options_html", time_format_options_html != NULL ? time_format_options_html : "")) {
10672 goto business_edit_cleanup;
10673 }
10674 }
10676business_edit_cleanup:
10677 text_buffer_free_runtime(&buffer);
10678 free(name); free(description); free(address); free(raw_image); free(image_url); free(current_logo_html); free(public_options_html); free(time_format_options_html); free(time_format);
10680 return code;
10681}
10682
10684 const char *state_path,
10685 const char *app_root
10686) {
10687 yyjson_doc *state_doc = NULL;
10688 yyjson_val *root = NULL;
10689 yyjson_val *businesses = NULL;
10690 yyjson_arr_iter iter;
10691 yyjson_val *row = NULL;
10692 yyjson_mut_doc *page_doc = NULL;
10693 yyjson_mut_val *page_obj = NULL;
10694 TextBufferRuntime cards = {0};
10695 char *result = NULL;
10696
10697 if (state_path == NULL || state_path[0] == '\0' || app_root == NULL || app_root[0] == '\0') {
10698 return NULL;
10699 }
10700
10701 state_doc = native_json_doc_load_file(state_path);
10702 root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
10703 if (root == NULL) {
10704 native_json_doc_free(state_doc);
10705 return NULL;
10706 }
10707
10708 businesses = state_array_runtime(root, "businesses");
10709 if (businesses != NULL && yyjson_arr_iter_init(businesses, &iter)) {
10710 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
10711 yyjson_val *public_value = NULL;
10712 yyjson_mut_doc *doc = NULL;
10713 yyjson_mut_val *params = NULL;
10714 char *slug = NULL;
10715 char *name = NULL;
10716 char *description = NULL;
10717 char *raw_image_url = NULL;
10718 char *business_image_url = NULL;
10719 char *html = NULL;
10720
10721 if (!yyjson_is_obj(row)) {
10722 continue;
10723 }
10724 public_value = native_json_obj_get(row, "is_public");
10725 if (!(yyjson_is_bool(public_value) && yyjson_get_bool(public_value))) {
10726 continue;
10727 }
10728 slug = native_json_obj_get_string_dup(row, "slug");
10729 if (slug == NULL || slug[0] == '\0') {
10730 free(slug);
10731 continue;
10732 }
10733 name = native_json_obj_get_string_dup(row, "name");
10734 description = native_json_obj_get_string_dup(row, "description");
10735 raw_image_url = native_json_obj_get_string_dup(row, "image_url");
10736 business_image_url = business_media_url_dup_runtime(slug, raw_image_url != NULL ? raw_image_url : "");
10737 doc = yyjson_mut_doc_new(NULL);
10738 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
10739 if (doc == NULL || params == NULL) {
10740 free(slug);
10741 free(name);
10742 free(description);
10743 free(raw_image_url);
10744 free(business_image_url);
10747 native_json_doc_free(state_doc);
10748 return NULL;
10749 }
10750 yyjson_mut_doc_set_root(doc, params);
10751 yyjson_mut_obj_add_strcpy(doc, params, "business_slug", slug != NULL ? slug : "");
10752 yyjson_mut_obj_add_strcpy(doc, params, "business_name", name != NULL ? name : "");
10753 yyjson_mut_obj_add_strcpy(doc, params, "business_description", description != NULL ? description : "");
10754 yyjson_mut_obj_add_strcpy(doc, params, "business_image_url", business_image_url != NULL ? business_image_url : "");
10755 html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/explore/fragments/business_card.html", doc);
10757 free(slug);
10758 free(name);
10759 free(description);
10760 free(raw_image_url);
10761 free(business_image_url);
10762 if (html == NULL || !text_buffer_append_text_runtime(&cards, html)) {
10763 free(html);
10765 native_json_doc_free(state_doc);
10766 return NULL;
10767 }
10768 free(html);
10769 }
10770 }
10771
10772 if (cards.length == 0) {
10773 if (!text_buffer_append_text_runtime(&cards, "<article class=\"card\"><p>No businesses available yet.</p></article>")) {
10775 native_json_doc_free(state_doc);
10776 return NULL;
10777 }
10778 }
10779
10780 page_doc = yyjson_mut_doc_new(NULL);
10781 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
10782 if (page_doc == NULL || page_obj == NULL
10783 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "business_cards_html", cards.text != NULL ? cards.text : "")) {
10785 yyjson_mut_doc_free(page_doc);
10786 native_json_doc_free(state_doc);
10787 return NULL;
10788 }
10789
10790 yyjson_mut_doc_set_root(page_doc, page_obj);
10791 result = yyjson_mut_write(page_doc, YYJSON_WRITE_NOFLAG, NULL);
10793 yyjson_mut_doc_free(page_doc);
10794 native_json_doc_free(state_doc);
10795 return result;
10796}
10797
10799 const char *state_path,
10800 const char *business_slug,
10801 long user_id,
10802 const char *app_root
10803) {
10804 yyjson_doc *state_doc = NULL;
10805 yyjson_val *root = NULL;
10806 yyjson_val *business = NULL;
10807 yyjson_val *subscriptions = NULL;
10808 yyjson_val *ratings = NULL;
10809 yyjson_val *services = NULL;
10810 business_admin_service_list_runtime service_list = {0};
10811 yyjson_mut_doc *page_doc = NULL;
10812 yyjson_mut_val *page_obj = NULL;
10813 yyjson_arr_iter iter;
10814 yyjson_val *row = NULL;
10815 TextBufferRuntime rating_rows = {0};
10816 TextBufferRuntime service_cards = {0};
10817 char *business_name = NULL;
10818 char *business_description = NULL;
10819 char *raw_business_image = NULL;
10820 char *business_image_url = NULL;
10821 char *business_image_html = NULL;
10822 char *rating_form_html = NULL;
10823 char *cta_html = NULL;
10824 char *empty_html = NULL;
10825 char *result = NULL;
10826 long business_id = 0;
10827 int subscribed = 0;
10828
10829 if (state_path == NULL || state_path[0] == '\0'
10830 || business_slug == NULL || business_slug[0] == '\0'
10831 || app_root == NULL || app_root[0] == '\0') {
10832 return NULL;
10833 }
10834
10835 state_doc = native_json_doc_load_file(state_path);
10836 root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
10837 business = root != NULL ? state_find_business_by_slug_runtime(root, business_slug) : NULL;
10838 if (root == NULL || business == NULL) {
10839 native_json_doc_free(state_doc);
10840 return NULL;
10841 }
10842
10843 subscriptions = state_array_runtime(root, "subscriptions");
10844 ratings = state_array_runtime(root, "ratings");
10845 services = state_array_runtime(root, "services");
10846 business_id = native_json_obj_get_long_default(business, "id", 0, NULL);
10847 business_name = native_json_obj_get_string_dup(business, "name");
10848 business_description = native_json_obj_get_string_dup(business, "description");
10849 raw_business_image = native_json_obj_get_string_dup(business, "image_url");
10850 business_image_url = business_media_url_dup_runtime(business_slug, raw_business_image != NULL ? raw_business_image : "");
10851
10852 if (subscriptions != NULL && yyjson_arr_iter_init(subscriptions, &iter)) {
10853 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
10854 if (yyjson_is_obj(row)
10855 && value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)
10856 && value_long_equals_runtime(native_json_obj_get(row, "user_id"), user_id)) {
10857 subscribed = 1;
10858 break;
10859 }
10860 }
10861 }
10862
10863 if (business_image_url != NULL && business_image_url[0] != '\0') {
10865 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
10866 if (doc == NULL || params == NULL) {
10868 goto business_landing_cleanup;
10869 }
10870 yyjson_mut_doc_set_root(doc, params);
10871 yyjson_mut_obj_add_strcpy(doc, params, "image_url", business_image_url);
10872 yyjson_mut_obj_add_strcpy(doc, params, "image_alt", business_name != NULL ? business_name : "");
10873 business_image_html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/business_landing_image.html", doc);
10875 if (business_image_html == NULL) {
10876 goto business_landing_cleanup;
10877 }
10878 }
10879
10880 if (ratings != NULL && yyjson_arr_iter_init(ratings, &iter)) {
10881 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
10882 yyjson_mut_doc *doc = NULL;
10883 yyjson_mut_val *params = NULL;
10884 char *comment = NULL;
10885 char *html = NULL;
10886 long score = 0;
10887 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "business_id"), business_id)) {
10888 continue;
10889 }
10890 score = native_json_obj_get_long_default(row, "score", 0, NULL);
10891 comment = native_json_obj_get_string_dup(row, "comment");
10892 doc = yyjson_mut_doc_new(NULL);
10893 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
10894 if (doc == NULL || params == NULL) {
10895 free(comment);
10897 goto business_landing_cleanup;
10898 }
10899 yyjson_mut_doc_set_root(doc, params);
10900 yyjson_mut_obj_add_int(doc, params, "rating_score", score);
10901 yyjson_mut_obj_add_strcpy(doc, params, "rating_comment", comment != NULL ? comment : "");
10902 html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/business_rating_row.html", doc);
10904 free(comment);
10905 if (html == NULL || !text_buffer_append_text_runtime(&rating_rows, html)) {
10906 free(html);
10907 goto business_landing_cleanup;
10908 }
10909 free(html);
10910 }
10911 }
10912
10913 if (rating_rows.length == 0) {
10915 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
10916 if (doc == NULL || params == NULL) {
10918 goto business_landing_cleanup;
10919 }
10920 yyjson_mut_doc_set_root(doc, params);
10921 empty_html = render_business_fragment_dup_runtime(app_root, "templates/businesses/fragments/business_rating_empty.html", (yyjson_val *)params);
10923 if (empty_html == NULL || !text_buffer_append_text_runtime(&rating_rows, empty_html)) {
10924 free(empty_html);
10925 goto business_landing_cleanup;
10926 }
10927 free(empty_html);
10928 empty_html = NULL;
10929 }
10930
10931 if (!collect_business_service_list_runtime(services, business_id, &service_list)) {
10932 goto business_landing_cleanup;
10933 }
10934 if (service_list.count > 0) {
10935 size_t index = 0;
10936 for (index = 0; index < service_list.count; index++) {
10937 yyjson_val *service = service_list.items[index].service;
10939 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
10940 char *service_name = native_json_obj_get_string_dup(service, "name");
10941 char *service_cost = native_json_obj_get_string_dup(service, "cost");
10942 char *service_description = native_json_obj_get_string_dup(service, "description");
10943 char *image_url_raw = native_json_obj_get_string_dup(service, "image_url");
10944 char *service_image_url = NULL;
10945 char *visual_html = NULL;
10946 char *card_html = NULL;
10947 long service_id = service_list.items[index].service_id;
10948 long duration_minutes = native_json_obj_get_long_default(service, "duration_minutes", 45, NULL);
10949 if (doc == NULL || params == NULL) {
10951 free(service_name);
10952 free(service_cost);
10953 free(service_description);
10954 free(image_url_raw);
10955 goto business_landing_cleanup;
10956 }
10957 if (image_url_raw == NULL || image_url_raw[0] == '\0') {
10958 free(image_url_raw);
10959 image_url_raw = native_json_obj_get_string_dup(service, "image");
10960 }
10961 service_image_url = service_media_url_dup_runtime(business_slug, image_url_raw != NULL ? image_url_raw : "");
10962 visual_html = render_service_visual_html_dup_runtime(app_root, service_image_url != NULL ? service_image_url : "", service_name != NULL ? service_name : "");
10963 if (duration_minutes <= 0) {
10964 duration_minutes = 45;
10965 }
10966 yyjson_mut_doc_set_root(doc, params);
10967 yyjson_mut_obj_add_strcpy(doc, params, "business_slug", business_slug != NULL ? business_slug : "");
10968 yyjson_mut_obj_add_int(doc, params, "service_id", service_id);
10969 yyjson_mut_obj_add_strcpy(doc, params, "service_visual_html", visual_html != NULL ? visual_html : "");
10970 yyjson_mut_obj_add_strcpy(doc, params, "service_name", service_name != NULL ? service_name : "");
10971 yyjson_mut_obj_add_int(doc, params, "service_duration_minutes", duration_minutes);
10972 yyjson_mut_obj_add_strcpy(doc, params, "service_cost", service_cost != NULL ? service_cost : "");
10973 yyjson_mut_obj_add_strcpy(doc, params, "service_description", service_description != NULL ? service_description : "");
10974 card_html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/business_service_card.html", doc);
10976 free(service_name);
10977 free(service_cost);
10978 free(service_description);
10979 free(image_url_raw);
10980 free(service_image_url);
10981 free(visual_html);
10982 if (card_html == NULL || !text_buffer_append_text_runtime(&service_cards, card_html)) {
10983 free(card_html);
10984 goto business_landing_cleanup;
10985 }
10986 free(card_html);
10987 }
10988 }
10989
10990 if (service_cards.length == 0) {
10992 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
10993 if (doc == NULL || params == NULL) {
10995 goto business_landing_cleanup;
10996 }
10997 yyjson_mut_doc_set_root(doc, params);
10998 empty_html = render_business_fragment_dup_runtime(app_root, "templates/businesses/fragments/business_services_empty.html", (yyjson_val *)params);
11000 if (empty_html == NULL || !text_buffer_append_text_runtime(&service_cards, empty_html)) {
11001 free(empty_html);
11002 goto business_landing_cleanup;
11003 }
11004 free(empty_html);
11005 empty_html = NULL;
11006 }
11007
11008 if (user_id > 0) {
11010 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
11011 if (doc == NULL || params == NULL) {
11013 goto business_landing_cleanup;
11014 }
11015 yyjson_mut_doc_set_root(doc, params);
11016 yyjson_mut_obj_add_strcpy(doc, params, "business_slug", business_slug != NULL ? business_slug : "");
11017 rating_form_html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/business_rating_form.html", doc);
11019 if (rating_form_html == NULL) {
11020 goto business_landing_cleanup;
11021 }
11022 }
11023
11024 {
11026 yyjson_mut_val *params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
11027 const char *fragment_path = NULL;
11028 if (doc == NULL || params == NULL) {
11030 goto business_landing_cleanup;
11031 }
11032 yyjson_mut_doc_set_root(doc, params);
11033 yyjson_mut_obj_add_strcpy(doc, params, "business_slug", business_slug != NULL ? business_slug : "");
11034 yyjson_mut_obj_add_strcpy(doc, params, "business_name", business_name != NULL ? business_name : "");
11035 if (user_id <= 0) {
11036 fragment_path = "templates/businesses/fragments/business_logged_out_cta.html";
11037 } else if (subscribed) {
11038 fragment_path = "templates/businesses/fragments/business_subscribed_cta.html";
11039 } else {
11040 fragment_path = "templates/businesses/fragments/business_subscribe_cta.html";
11041 }
11042 cta_html = render_business_fragment_from_doc_dup_runtime(app_root, fragment_path, doc);
11044 if (cta_html == NULL) {
11045 goto business_landing_cleanup;
11046 }
11047 }
11048
11049 page_doc = yyjson_mut_doc_new(NULL);
11050 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
11051 if (page_doc == NULL || page_obj == NULL) {
11052 goto business_landing_cleanup;
11053 }
11054 yyjson_mut_doc_set_root(page_doc, page_obj);
11055 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "business_name", business_name != NULL ? business_name : "");
11056 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "business_image_html", business_image_html != NULL ? business_image_html : "");
11057 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "business_description", business_description != NULL ? business_description : "");
11058 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "cta_html", cta_html != NULL ? cta_html : "");
11059 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "service_cards_html", service_cards.text != NULL ? service_cards.text : "");
11060 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "rating_rows_html", rating_rows.text != NULL ? rating_rows.text : "");
11061 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "rating_form_html", rating_form_html != NULL ? rating_form_html : "");
11062 result = yyjson_mut_write(page_doc, YYJSON_WRITE_NOFLAG, NULL);
11063
11064business_landing_cleanup:
11066 text_buffer_free_runtime(&rating_rows);
11067 text_buffer_free_runtime(&service_cards);
11068 free(business_name);
11069 free(business_description);
11070 free(raw_business_image);
11071 free(business_image_url);
11072 free(business_image_html);
11073 free(rating_form_html);
11074 free(cta_html);
11075 yyjson_mut_doc_free(page_doc);
11076 native_json_doc_free(state_doc);
11077 return result;
11078}
11079
11081 const char *state_path,
11082 long user_id,
11083 const char *app_root
11084) {
11085 yyjson_doc *state_doc = NULL;
11086 yyjson_val *root = NULL;
11087 yyjson_val *appointments = NULL;
11088 yyjson_arr_iter iter;
11089 yyjson_val *row = NULL;
11090 yyjson_mut_doc *page_doc = NULL;
11091 yyjson_mut_val *page_obj = NULL;
11092 TextBufferRuntime appointment_items = {0};
11093 const char *heading = "Appointments";
11094 const char *tips_block = "";
11095 int managed_mode = 0;
11096 char *result = NULL;
11097 if (state_path == NULL || state_path[0] == '\0' || app_root == NULL || app_root[0] == '\0') {
11098 return NULL;
11099 }
11100 state_doc = native_json_doc_load_file(state_path);
11101 root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
11102 if (root == NULL) {
11103 native_json_doc_free(state_doc);
11104 return NULL;
11105 }
11106 if (state_array_runtime(root, "tenant_memberships") != NULL) {
11107 yyjson_val *all_appointments = state_array_runtime(root, "appointments");
11108 yyjson_arr_iter managed_iter;
11109 yyjson_val *managed_row = NULL;
11110 if (all_appointments != NULL && yyjson_arr_iter_init(all_appointments, &managed_iter)) {
11111 while ((managed_row = yyjson_arr_iter_next(&managed_iter)) != NULL) {
11112 long business_id = yyjson_is_obj(managed_row) ? native_json_obj_get_long_default(managed_row, "business_id", 0, NULL) : 0;
11113 if (business_id > 0 && user_manages_business_runtime(root, user_id, business_id)) {
11114 managed_mode = 1;
11115 break;
11116 }
11117 }
11118 }
11119 }
11120 if (managed_mode) {
11121 heading = "Manage Bookings";
11122 }
11123 appointments = state_array_runtime(root, "appointments");
11124 if (appointments != NULL && yyjson_arr_iter_init(appointments, &iter)) {
11125 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
11126 yyjson_mut_doc *doc = NULL;
11127 yyjson_mut_val *params = NULL;
11128 yyjson_val *business = NULL;
11129 yyjson_val *service = NULL;
11130 yyjson_val *customer = NULL;
11131 yyjson_val *technician = NULL;
11132 long appointment_id = 0;
11133 long business_id = 0;
11134 long service_id = 0;
11135 long customer_id = 0;
11136 long technician_id = 0;
11137 char *business_name = NULL;
11138 char *service_name = NULL;
11139 char *customer_name = NULL;
11140 char *technician_name = NULL;
11141 char *date_value = NULL;
11142 char *time_value = NULL;
11143 char *status_value = NULL;
11144 char *technician_html = NULL;
11145 char *html = NULL;
11146 TextBufferRuntime tech = {0};
11147 const char *fragment_path = NULL;
11148 if (!yyjson_is_obj(row)) {
11149 continue;
11150 }
11151 appointment_id = native_json_obj_get_long_default(row, "id", 0, NULL);
11152 business_id = native_json_obj_get_long_default(row, "business_id", 0, NULL);
11153 service_id = native_json_obj_get_long_default(row, "service_id", 0, NULL);
11154 customer_id = native_json_obj_get_long_default(row, "user_id", 0, NULL);
11155 technician_id = native_json_obj_get_long_default(row, "technician_id", 0, NULL);
11156 if (managed_mode) {
11157 if (!user_manages_business_runtime(root, user_id, business_id)) {
11158 continue;
11159 }
11160 } else if (customer_id != user_id) {
11161 continue;
11162 }
11163 business = state_find_business_by_id_runtime(root, business_id);
11164 service = state_find_service_by_id_runtime(root, service_id);
11165 customer = state_find_user_by_id_runtime(root, customer_id);
11166 technician = state_find_technician_for_business_runtime(root, technician_id, business_id);
11167 business_name = business != NULL ? native_json_obj_get_string_dup(business, "name") : NULL;
11168 service_name = service != NULL ? native_json_obj_get_string_dup(service, "name") : NULL;
11169 customer_name = customer != NULL ? native_json_obj_get_string_dup(customer, "full_name") : NULL;
11170 technician_name = technician != NULL ? native_json_obj_get_string_dup(technician, "name") : NULL;
11171 date_value = native_json_obj_get_string_dup(row, "date");
11172 time_value = native_json_obj_get_string_dup(row, "time");
11173 status_value = native_json_obj_get_string_dup(row, "status");
11174 if (technician_name != NULL && technician_name[0] != '\0') {
11175 if (managed_mode) {
11176 if (!text_buffer_append_text_runtime(&tech, "<br><span class=\"muted\">Technician: ")
11177 || !text_buffer_append_html_escaped_runtime(&tech, technician_name)
11178 || !text_buffer_append_text_runtime(&tech, "</span>")) {
11179 free(business_name); free(service_name); free(customer_name); free(technician_name); free(date_value); free(time_value); free(status_value);
11180 text_buffer_free_runtime(&tech); text_buffer_free_runtime(&appointment_items);
11181 native_json_doc_free(state_doc);
11182 return NULL;
11183 }
11184 } else {
11185 if (!text_buffer_append_text_runtime(&tech, "<br>👤 Technician: ")
11186 || !text_buffer_append_html_escaped_runtime(&tech, technician_name)) {
11187 free(business_name); free(service_name); free(customer_name); free(technician_name); free(date_value); free(time_value); free(status_value);
11188 text_buffer_free_runtime(&tech); text_buffer_free_runtime(&appointment_items);
11189 native_json_doc_free(state_doc);
11190 return NULL;
11191 }
11192 }
11193 technician_html = text_buffer_take_runtime(&tech);
11194 }
11195 doc = yyjson_mut_doc_new(NULL);
11196 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
11197 if (doc == NULL || params == NULL) {
11198 free(business_name); free(service_name); free(customer_name); free(technician_name); free(date_value); free(time_value); free(status_value); free(technician_html);
11199 yyjson_mut_doc_free(doc); text_buffer_free_runtime(&appointment_items);
11200 native_json_doc_free(state_doc);
11201 return NULL;
11202 }
11203 yyjson_mut_doc_set_root(doc, params);
11204 yyjson_mut_obj_add_strcpy(doc, params, "service_name", service_name != NULL ? service_name : "");
11205 yyjson_mut_obj_add_strcpy(doc, params, "business_name", business_name != NULL ? business_name : "");
11206 yyjson_mut_obj_add_strcpy(doc, params, "date_value", date_value != NULL ? date_value : "");
11207 yyjson_mut_obj_add_strcpy(doc, params, "time_value", time_value != NULL ? time_value : "");
11208 yyjson_mut_obj_add_strcpy(doc, params, "status_value", status_value != NULL ? status_value : "");
11209 yyjson_mut_obj_add_strcpy(doc, params, "technician_html", technician_html != NULL ? technician_html : "");
11210 if (managed_mode) {
11211 char complete_path[128];
11212 char cancel_path[128];
11213 yyjson_mut_obj_add_strcpy(doc, params, "customer_name", customer_name != NULL ? customer_name : "");
11214 snprintf(complete_path, sizeof(complete_path), "/appointments/%ld/complete/", appointment_id);
11215 snprintf(cancel_path, sizeof(cancel_path), "/appointments/%ld/cancel/", appointment_id);
11216 yyjson_mut_obj_add_strcpy(doc, params, "complete_path", complete_path);
11217 yyjson_mut_obj_add_strcpy(doc, params, "cancel_path", cancel_path);
11218 fragment_path = "templates/appointments/fragments/managed_appointment_row.html";
11219 } else {
11220 char detail_path[128];
11221 char request_cancel_path[128];
11222 snprintf(detail_path, sizeof(detail_path), "/appointments/%ld/", appointment_id);
11223 snprintf(request_cancel_path, sizeof(request_cancel_path), "/appointments/%ld/request-cancel/", appointment_id);
11224 yyjson_mut_obj_add_strcpy(doc, params, "detail_path", detail_path);
11225 yyjson_mut_obj_add_strcpy(doc, params, "request_cancel_path", request_cancel_path);
11226 fragment_path = "templates/appointments/fragments/customer_appointment_row.html";
11227 }
11228 html = render_business_fragment_from_doc_dup_runtime(app_root, fragment_path, doc);
11230 free(business_name); free(service_name); free(customer_name); free(technician_name); free(date_value); free(time_value); free(status_value); free(technician_html);
11231 if (html == NULL || !text_buffer_append_text_runtime(&appointment_items, html)) {
11232 free(html);
11233 text_buffer_free_runtime(&appointment_items);
11234 native_json_doc_free(state_doc);
11235 return NULL;
11236 }
11237 free(html);
11238 }
11239 }
11240 if (appointment_items.length == 0) {
11241 if (!text_buffer_append_text_runtime(&appointment_items, "<li><p>No appointments. <a href=\"/explore/\">Browse businesses</a> to book.</p></li>")) {
11242 text_buffer_free_runtime(&appointment_items);
11243 native_json_doc_free(state_doc);
11244 return NULL;
11245 }
11246 }
11247 if (!managed_mode) {
11248 tips_block = "<div class=\"card\" style=\"margin-top:2rem;background:var(--gray-50);\"><h3>Tips</h3><ul style=\"margin:0.5rem 0;padding-left:1.5rem;\"><li>Use this page to review your active appointments and request cancellations</li><li>Contact the business owner directly using the contact details shown in appointment detail</li><li>If you need to cancel, do so as early as possible</li></ul></div>";
11249 }
11250 page_doc = yyjson_mut_doc_new(NULL);
11251 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
11252 if (page_doc == NULL || page_obj == NULL
11253 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "heading", heading)
11254 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "appointment_items", appointment_items.text != NULL ? appointment_items.text : "")
11255 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "tips_block", tips_block)) {
11256 text_buffer_free_runtime(&appointment_items);
11257 yyjson_mut_doc_free(page_doc);
11258 native_json_doc_free(state_doc);
11259 return NULL;
11260 }
11261 yyjson_mut_doc_set_root(page_doc, page_obj);
11262 result = yyjson_mut_write(page_doc, YYJSON_WRITE_NOFLAG, NULL);
11263 text_buffer_free_runtime(&appointment_items);
11264 yyjson_mut_doc_free(page_doc);
11265 native_json_doc_free(state_doc);
11266 return result;
11267}
11268
11270 typedef struct {
11271 char *name;
11272 long count;
11273 } RuntimeStatusCount;
11274 yyjson_doc *state_doc = NULL;
11275 yyjson_val *root = NULL;
11276 yyjson_val *appointments = NULL;
11277 yyjson_val *bookings = NULL;
11278 yyjson_val *subscriptions = NULL;
11279 yyjson_val *ratings = NULL;
11280 yyjson_val *connections = NULL;
11281 yyjson_val *audit_log = NULL;
11282 yyjson_arr_iter iter;
11283 yyjson_val *row = NULL;
11284 yyjson_mut_doc *page_doc = NULL;
11285 yyjson_mut_val *page_obj = NULL;
11286 TextBufferRuntime status_rows = {0};
11287 TextBufferRuntime audit_rows = {0};
11288 RuntimeStatusCount statuses[32] = {0};
11289 size_t status_count = 0;
11290 size_t audit_total = 0;
11291 size_t audit_start = 0;
11292 long appointment_count = 0;
11293 long booking_count = 0;
11294 long subscription_count = 0;
11295 long rating_count = 0;
11296 long connection_count = 0;
11297 long unassigned_count = 0;
11298 long cancel_requested_count = 0;
11299 char *result = NULL;
11300 size_t index = 0;
11301
11302 if (state_path == NULL || state_path[0] == '\0') {
11303 return NULL;
11304 }
11305
11306 state_doc = native_json_doc_load_file(state_path);
11307 root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
11308 if (root == NULL) {
11309 native_json_doc_free(state_doc);
11310 return NULL;
11311 }
11312
11313 appointments = state_array_runtime(root, "appointments");
11314 bookings = state_array_runtime(root, "bookings");
11315 subscriptions = state_array_runtime(root, "subscriptions");
11316 ratings = state_array_runtime(root, "ratings");
11317 connections = state_array_runtime(root, "google_calendar_connections");
11318 audit_log = state_array_runtime(root, "audit_log");
11319 appointment_count = appointments != NULL ? (long)yyjson_arr_size(appointments) : 0;
11320 booking_count = bookings != NULL ? (long)yyjson_arr_size(bookings) : 0;
11321 subscription_count = subscriptions != NULL ? (long)yyjson_arr_size(subscriptions) : 0;
11322 rating_count = ratings != NULL ? (long)yyjson_arr_size(ratings) : 0;
11323 connection_count = connections != NULL ? (long)yyjson_arr_size(connections) : 0;
11324
11325 if (appointments != NULL && yyjson_arr_iter_init(appointments, &iter)) {
11326 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
11327 char *status = NULL;
11328 long technician_id = 0;
11329 size_t slot = 0;
11330 int found = 0;
11331 if (!yyjson_is_obj(row)) {
11332 continue;
11333 }
11334 technician_id = native_json_obj_get_long_default(row, "technician_id", 0, NULL);
11335 status = native_json_obj_get_string_dup(row, "status");
11336 if (technician_id <= 0) {
11337 unassigned_count++;
11338 }
11339 if (status != NULL && streq(status, "cancel_requested")) {
11340 cancel_requested_count++;
11341 }
11342 for (slot = 0; slot < status_count; slot++) {
11343 if (statuses[slot].name != NULL && status != NULL && streq(statuses[slot].name, status)) {
11344 statuses[slot].count++;
11345 found = 1;
11346 break;
11347 }
11348 }
11349 if (!found && status != NULL && status_count < (sizeof(statuses) / sizeof(statuses[0]))) {
11350 statuses[status_count].name = status;
11351 statuses[status_count].count = 1;
11352 status = NULL;
11353 status_count++;
11354 }
11355 free(status);
11356 }
11357 }
11358
11359 if (status_count == 0) {
11360 if (!text_buffer_append_text_runtime(&status_rows, "<li>No appointments yet.</li>")) {
11361 goto diagnostics_render_cleanup;
11362 }
11363 } else {
11364 for (index = 0; index < status_count; index++) {
11365 if (!text_buffer_append_text_runtime(&status_rows, "<li><code>")
11366 || !text_buffer_append_html_escaped_runtime(&status_rows, statuses[index].name != NULL ? statuses[index].name : "unknown")
11367 || !text_buffer_append_format_runtime(&status_rows, "</code>: %ld</li>", statuses[index].count)) {
11368 goto diagnostics_render_cleanup;
11369 }
11370 }
11371 }
11372
11373 audit_total = audit_log != NULL ? yyjson_arr_size(audit_log) : 0;
11374 audit_start = audit_total > 5 ? audit_total - 5 : 0;
11375 if (audit_log != NULL && yyjson_arr_iter_init(audit_log, &iter)) {
11376 size_t pos = 0;
11377 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
11378 char *event = NULL;
11379 char *resource_id = NULL;
11380 if (pos++ < audit_start || !yyjson_is_obj(row)) {
11381 continue;
11382 }
11383 event = native_json_obj_get_string_dup(row, "event");
11384 if (event == NULL) {
11385 event = native_json_obj_get_string_dup(row, "transition_id");
11386 }
11387 resource_id = native_json_obj_get_string_dup(row, "resource_id");
11388 if (!text_buffer_append_text_runtime(&audit_rows, "<li>")
11389 || !text_buffer_append_html_escaped_runtime(&audit_rows, event != NULL ? event : "audit_event")
11390 || ((resource_id != NULL && resource_id[0] != '\0')
11391 && (!text_buffer_append_text_runtime(&audit_rows, " · ")
11392 || !text_buffer_append_html_escaped_runtime(&audit_rows, resource_id)))
11393 || !text_buffer_append_text_runtime(&audit_rows, "</li>")) {
11394 free(event);
11395 free(resource_id);
11396 goto diagnostics_render_cleanup;
11397 }
11398 free(event);
11399 free(resource_id);
11400 }
11401 }
11402 if (audit_rows.length == 0 && !text_buffer_append_text_runtime(&audit_rows, "<li>No audit events.</li>")) {
11403 goto diagnostics_render_cleanup;
11404 }
11405
11406 page_doc = yyjson_mut_doc_new(NULL);
11407 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
11408 if (page_doc == NULL || page_obj == NULL) {
11409 goto diagnostics_render_cleanup;
11410 }
11411 yyjson_mut_doc_set_root(page_doc, page_obj);
11412 yyjson_mut_obj_add_int(page_doc, page_obj, "appointment_count", appointment_count);
11413 yyjson_mut_obj_add_int(page_doc, page_obj, "booking_count", booking_count);
11414 yyjson_mut_obj_add_int(page_doc, page_obj, "subscription_count", subscription_count);
11415 yyjson_mut_obj_add_int(page_doc, page_obj, "rating_count", rating_count);
11416 yyjson_mut_obj_add_int(page_doc, page_obj, "unassigned_count", unassigned_count);
11417 yyjson_mut_obj_add_int(page_doc, page_obj, "cancel_requested_count", cancel_requested_count);
11418 yyjson_mut_obj_add_int(page_doc, page_obj, "connection_count", connection_count);
11419 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "appointment_statuses", status_rows.text != NULL ? status_rows.text : "");
11420 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "recent_audit", audit_rows.text != NULL ? audit_rows.text : "");
11421 result = yyjson_mut_write(page_doc, YYJSON_WRITE_NOFLAG, NULL);
11422
11423 diagnostics_render_cleanup:
11424 for (index = 0; index < status_count; index++) {
11425 free(statuses[index].name);
11426 }
11427 text_buffer_free_runtime(&status_rows);
11428 text_buffer_free_runtime(&audit_rows);
11429 yyjson_mut_doc_free(page_doc);
11430 native_json_doc_free(state_doc);
11431 return result;
11432}
11433
11435 const char *state_path,
11436 long user_id,
11437 const char *app_root
11438) {
11439 yyjson_doc *state_doc = NULL;
11440 yyjson_val *root = NULL;
11441 yyjson_val *businesses = NULL;
11442 yyjson_arr_iter iter;
11443 yyjson_val *row = NULL;
11444 yyjson_mut_doc *page_doc = NULL;
11445 yyjson_mut_val *page_obj = NULL;
11446 TextBufferRuntime cards = {0};
11447 char *result = NULL;
11448 if (state_path == NULL || state_path[0] == '\0' || app_root == NULL || app_root[0] == '\0') {
11449 return NULL;
11450 }
11451 state_doc = native_json_doc_load_file(state_path);
11452 root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
11453 if (root == NULL) {
11454 native_json_doc_free(state_doc);
11455 return NULL;
11456 }
11457 businesses = state_array_runtime(root, "businesses");
11458 if (businesses != NULL && yyjson_arr_iter_init(businesses, &iter)) {
11459 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
11460 yyjson_mut_doc *doc = NULL;
11461 yyjson_mut_val *params = NULL;
11462 long business_id = 0;
11463 char *name = NULL;
11464 char *description = NULL;
11465 char *html = NULL;
11466 if (!yyjson_is_obj(row)) {
11467 continue;
11468 }
11469 business_id = native_json_obj_get_long_default(row, "id", 0, NULL);
11470 if (!user_manages_business_runtime(root, user_id, business_id)) {
11471 continue;
11472 }
11473 name = native_json_obj_get_string_dup(row, "name");
11474 description = native_json_obj_get_string_dup(row, "description");
11475 doc = yyjson_mut_doc_new(NULL);
11476 params = doc != NULL ? yyjson_mut_obj(doc) : NULL;
11477 if (doc == NULL || params == NULL) {
11478 free(name);
11479 free(description);
11482 native_json_doc_free(state_doc);
11483 return NULL;
11484 }
11485 yyjson_mut_doc_set_root(doc, params);
11486 yyjson_mut_obj_add_strcpy(doc, params, "business_slug", yyjson_get_str(native_json_obj_get(row, "slug")) != NULL ? yyjson_get_str(native_json_obj_get(row, "slug")) : "");
11487 yyjson_mut_obj_add_strcpy(doc, params, "business_name", name != NULL ? name : "");
11488 yyjson_mut_obj_add_strcpy(doc, params, "business_description", description != NULL ? description : "");
11489 html = render_business_fragment_from_doc_dup_runtime(app_root, "templates/businesses/fragments/business_dashboard_card.html", doc);
11491 free(name);
11492 free(description);
11493 if (html == NULL || !text_buffer_append_text_runtime(&cards, html)) {
11494 free(html);
11496 native_json_doc_free(state_doc);
11497 return NULL;
11498 }
11499 free(html);
11500 }
11501 }
11502 page_doc = yyjson_mut_doc_new(NULL);
11503 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
11504 if (page_doc == NULL || page_obj == NULL
11505 || !yyjson_mut_obj_add_strcpy(page_doc, page_obj, "business_cards_html", cards.text != NULL ? cards.text : "")) {
11507 yyjson_mut_doc_free(page_doc);
11508 native_json_doc_free(state_doc);
11509 return NULL;
11510 }
11511 yyjson_mut_doc_set_root(page_doc, page_obj);
11512 result = yyjson_mut_write(page_doc, YYJSON_WRITE_NOFLAG, NULL);
11514 yyjson_mut_doc_free(page_doc);
11515 native_json_doc_free(state_doc);
11516 return result;
11517}
11518
11520 const char *state_path,
11521 const char *business_slug,
11522 const char *app_root
11523) {
11524 static const char *invite_account_type_options_html =
11525 "<option value=\"business_customer\" selected>Customer</option>"
11526 "<option value=\"business_manager\">Manager</option>"
11527 "<option value=\"business_technician\">Technician</option>";
11528 yyjson_doc *state_doc = NULL;
11529 yyjson_val *root = NULL;
11530 yyjson_val *business = NULL;
11531 yyjson_val *services = NULL;
11532 yyjson_val *technicians = NULL;
11533 yyjson_val *availability_rows = NULL;
11534 yyjson_arr_iter iter;
11535 yyjson_val *row = NULL;
11536 yyjson_mut_doc *page_doc = NULL;
11537 yyjson_mut_val *page_obj = NULL;
11538 TextBufferRuntime service_rows = {0};
11539 TextBufferRuntime technician_rows = {0};
11540 TextBufferRuntime availability_html = {0};
11541 char *business_name = NULL;
11542 char *new_service_sort_order_options_html = NULL;
11543 char *new_service_booking_options_editor_html = NULL;
11544 char *service_technician_checkboxes_html = NULL;
11545 char *availability_day_options_html = NULL;
11546 char *result = NULL;
11547 long business_id = 0;
11548 long service_count = 0;
11549 long technician_count = 0;
11550 long appointment_count = 0;
11551 long unassigned_count = 0;
11552 int stripe_connected = 0;
11553 if (state_path == NULL || state_path[0] == '\0' || business_slug == NULL || business_slug[0] == '\0' || app_root == NULL || app_root[0] == '\0') {
11554 return NULL;
11555 }
11556 state_doc = native_json_doc_load_file(state_path);
11557 root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
11558 business = root != NULL ? state_find_business_by_slug_runtime(root, business_slug) : NULL;
11559 if (root == NULL || business == NULL) {
11560 native_json_doc_free(state_doc);
11561 return NULL;
11562 }
11563 business_id = native_json_obj_get_long_default(business, "id", 0, NULL);
11564 business_name = native_json_obj_get_string_dup(business, "name");
11565 stripe_connected = json_value_truthy_runtime(native_json_obj_get(business, "stripe_connected"));
11566 services = state_array_runtime(root, "services");
11567 if (services != NULL && yyjson_arr_iter_init(services, &iter)) {
11568 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
11569 char *name = NULL;
11570 char *cost = NULL;
11571 long row_business_id = 0;
11572 long service_id = 0;
11573 if (!yyjson_is_obj(row)) {
11574 continue;
11575 }
11576 row_business_id = native_json_obj_get_long_default(row, "business_id", 0, NULL);
11577 if (row_business_id != business_id) {
11578 continue;
11579 }
11580 service_count++;
11581 service_id = native_json_obj_get_long_default(row, "id", 0, NULL);
11582 name = native_json_obj_get_string_dup(row, "name");
11583 cost = native_json_obj_get_string_dup(row, "cost");
11584 if (!text_buffer_append_format_runtime(&service_rows, "<div class=\"card\"><strong>%s</strong><br><span class=\"muted\">%s</span><br><a href=\"/businesses/%s/services/%ld/edit/\">Edit</a></div>", name != NULL ? name : "", cost != NULL ? cost : "", business_slug, service_id)) {
11585 free(name); free(cost); goto business_detail_admin_cleanup;
11586 }
11587 free(name); free(cost);
11588 }
11589 }
11590 technicians = state_array_runtime(root, "technicians");
11591 if (technicians != NULL && yyjson_arr_iter_init(technicians, &iter)) {
11592 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
11593 char *name = NULL;
11594 char *email = NULL;
11595 long row_business_id = 0;
11596 long technician_id = 0;
11597 if (!yyjson_is_obj(row)) {
11598 continue;
11599 }
11600 row_business_id = native_json_obj_get_long_default(row, "business_id", 0, NULL);
11601 if (row_business_id != business_id) {
11602 continue;
11603 }
11604 technician_count++;
11605 technician_id = native_json_obj_get_long_default(row, "id", 0, NULL);
11606 name = native_json_obj_get_string_dup(row, "name");
11607 email = native_json_obj_get_string_dup(row, "email");
11608 if (!text_buffer_append_format_runtime(&technician_rows, "<div class=\"card\"><strong>%s</strong><br><span class=\"muted\">%s</span><br><a href=\"/businesses/%s/technicians/%ld/edit/\">Edit</a></div>", name != NULL ? name : "", email != NULL ? email : "", business_slug, technician_id)) {
11609 free(name); free(email); goto business_detail_admin_cleanup;
11610 }
11611 free(name); free(email);
11612 }
11613 }
11614 availability_rows = state_array_runtime(root, "business_availability");
11615 if (availability_rows != NULL && yyjson_arr_iter_init(availability_rows, &iter)) {
11616 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
11617 char *day = NULL;
11618 char *start_time = NULL;
11619 char *end_time = NULL;
11620 long row_business_id = 0;
11621 long availability_id = 0;
11622 if (!yyjson_is_obj(row)) {
11623 continue;
11624 }
11625 row_business_id = native_json_obj_get_long_default(row, "business_id", 0, NULL);
11626 if (row_business_id != business_id) {
11627 continue;
11628 }
11629 availability_id = native_json_obj_get_long_default(row, "id", 0, NULL);
11630 day = native_json_obj_get_string_dup(row, "day_of_week");
11631 start_time = native_json_obj_get_string_dup(row, "start_time");
11632 end_time = native_json_obj_get_string_dup(row, "end_time");
11633 if (!text_buffer_append_format_runtime(&availability_html, "<div class=\"card\"><strong>%s</strong> %s-%s<br><a href=\"/businesses/%s/availability/%ld/edit/\">Edit</a></div>", day != NULL ? day : "", start_time != NULL ? start_time : "", end_time != NULL ? end_time : "", business_slug, availability_id)) {
11634 free(day); free(start_time); free(end_time); goto business_detail_admin_cleanup;
11635 }
11636 free(day); free(start_time); free(end_time);
11637 }
11638 }
11639 {
11640 yyjson_val *appointments = state_array_runtime(root, "appointments");
11641 if (appointments != NULL && yyjson_arr_iter_init(appointments, &iter)) {
11642 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
11643 long row_business_id = 0;
11644 if (!yyjson_is_obj(row)) {
11645 continue;
11646 }
11647 row_business_id = native_json_obj_get_long_default(row, "business_id", 0, NULL);
11648 if (row_business_id == business_id) {
11649 appointment_count++;
11650 if (native_json_obj_get_long_default(row, "technician_id", 0, NULL) <= 0) {
11651 unassigned_count++;
11652 }
11653 }
11654 }
11655 }
11656 }
11657 new_service_sort_order_options_html = render_service_sort_order_options_html_dup_runtime(root, business_id, service_count + 1, 1, app_root);
11658 new_service_booking_options_editor_html = render_service_booking_options_editor_html_dup_runtime(root, 0, app_root);
11659 service_technician_checkboxes_html = render_technician_checkboxes_html_dup_runtime(root, business_id, 0, "Create technicians first to assign them to services.", 1, app_root);
11660 availability_day_options_html = day_of_week_options_html_dup_runtime("Mon");
11661 page_doc = yyjson_mut_doc_new(NULL);
11662 page_obj = page_doc != NULL ? yyjson_mut_obj(page_doc) : NULL;
11663 if (page_doc == NULL || page_obj == NULL || new_service_sort_order_options_html == NULL || new_service_booking_options_editor_html == NULL || service_technician_checkboxes_html == NULL || availability_day_options_html == NULL) {
11664 goto business_detail_admin_cleanup;
11665 }
11666 yyjson_mut_doc_set_root(page_doc, page_obj);
11667 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "business_name", business_name != NULL ? business_name : "");
11668 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "business_slug", business_slug);
11669 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "landing_path", business_slug != NULL ? "" : "");
11670 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "edit_path", business_slug != NULL ? "" : "");
11671 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "public_state_button_html", "");
11672 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "calendar_path", business_slug != NULL ? "" : "");
11673 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "delete_path", business_slug != NULL ? "" : "");
11674 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "public_url_path", business_slug != NULL ? "" : "");
11675 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "stripe_connected", stripe_connected ? "true" : "false");
11676 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "connect_stripe_path", business_slug != NULL ? "" : "");
11677 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "connection_status", "Disconnected");
11678 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "manage_path", business_slug != NULL ? "" : "");
11679 yyjson_mut_obj_add_int(page_doc, page_obj, "appointment_count", appointment_count);
11680 yyjson_mut_obj_add_int(page_doc, page_obj, "unassigned_count", unassigned_count);
11681 yyjson_mut_obj_add_int(page_doc, page_obj, "technician_count", technician_count);
11682 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "invite_create_path", business_slug != NULL ? "" : "");
11683 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "invite_account_type_options_html", invite_account_type_options_html);
11684 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "services_manage_path", business_slug != NULL ? "" : "");
11685 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "service_rows_html", service_rows.text != NULL ? service_rows.text : "");
11686 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "service_create_path", business_slug != NULL ? "" : "");
11687 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "new_service_sort_order_options_html", new_service_sort_order_options_html);
11688 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "new_service_booking_options_editor_html", new_service_booking_options_editor_html);
11689 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "service_technician_checkboxes_html", service_technician_checkboxes_html);
11690 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "technicians_manage_path", business_slug != NULL ? "" : "");
11691 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "technician_rows_html", technician_rows.text != NULL ? technician_rows.text : "");
11692 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "technician_create_path", business_slug != NULL ? "" : "");
11693 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "availability_manage_path", business_slug != NULL ? "" : "");
11694 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "availability_rows_html", availability_html.text != NULL ? availability_html.text : "");
11695 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "availability_create_path", business_slug != NULL ? "" : "");
11696 yyjson_mut_obj_add_strcpy(page_doc, page_obj, "availability_day_options_html", availability_day_options_html);
11697 {
11698 char path_text[512];
11699 snprintf(path_text, sizeof(path_text), "/b/%s/", business_slug);
11700 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "landing_path"), yyjson_mut_strcpy(page_doc, path_text));
11701 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "public_url_path"), yyjson_mut_strcpy(page_doc, path_text));
11702 snprintf(path_text, sizeof(path_text), "/businesses/%s/edit/", business_slug);
11703 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "edit_path"), yyjson_mut_strcpy(page_doc, path_text));
11704 snprintf(path_text, sizeof(path_text), "/business/%s/calendar/", business_slug);
11705 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "calendar_path"), yyjson_mut_strcpy(page_doc, path_text));
11706 snprintf(path_text, sizeof(path_text), "/businesses/%s/delete/", business_slug);
11707 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "delete_path"), yyjson_mut_strcpy(page_doc, path_text));
11708 snprintf(path_text, sizeof(path_text), "/businesses/%s/connect-stripe/", business_slug);
11709 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "connect_stripe_path"), yyjson_mut_strcpy(page_doc, path_text));
11710 snprintf(path_text, sizeof(path_text), "/businesses/%s/", business_slug);
11711 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "manage_path"), yyjson_mut_strcpy(page_doc, path_text));
11712 snprintf(path_text, sizeof(path_text), "/businesses/%s/invites/new/", business_slug);
11713 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "invite_create_path"), yyjson_mut_strcpy(page_doc, path_text));
11714 snprintf(path_text, sizeof(path_text), "/businesses/%s/services/", business_slug);
11715 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "services_manage_path"), yyjson_mut_strcpy(page_doc, path_text));
11716 snprintf(path_text, sizeof(path_text), "/businesses/%s/services/new/", business_slug);
11717 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "service_create_path"), yyjson_mut_strcpy(page_doc, path_text));
11718 snprintf(path_text, sizeof(path_text), "/businesses/%s/technicians/", business_slug);
11719 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "technicians_manage_path"), yyjson_mut_strcpy(page_doc, path_text));
11720 snprintf(path_text, sizeof(path_text), "/businesses/%s/technicians/new/", business_slug);
11721 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "technician_create_path"), yyjson_mut_strcpy(page_doc, path_text));
11722 snprintf(path_text, sizeof(path_text), "/businesses/%s/availability/", business_slug);
11723 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "availability_manage_path"), yyjson_mut_strcpy(page_doc, path_text));
11724 snprintf(path_text, sizeof(path_text), "/businesses/%s/availability/new/", business_slug);
11725 yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(page_doc, "availability_create_path"), yyjson_mut_strcpy(page_doc, path_text));
11726 }
11727 result = yyjson_mut_write(page_doc, YYJSON_WRITE_NOFLAG, NULL);
11728
11729business_detail_admin_cleanup:
11730 text_buffer_free_runtime(&service_rows);
11731 text_buffer_free_runtime(&technician_rows);
11732 text_buffer_free_runtime(&availability_html);
11733 free(business_name);
11734 free(new_service_sort_order_options_html);
11735 free(new_service_booking_options_editor_html);
11736 free(service_technician_checkboxes_html);
11737 free(availability_day_options_html);
11738 yyjson_mut_doc_free(page_doc);
11739 native_json_doc_free(state_doc);
11740 return result;
11741}
11742
11744 const char *state_path,
11745 const char *business_slug
11746) {
11747 yyjson_doc *state_doc = NULL;
11748 yyjson_val *root = NULL;
11749 yyjson_val *business = NULL;
11750 yyjson_mut_doc *doc = NULL;
11751 yyjson_mut_val *page = NULL;
11752 char *name = NULL;
11753 char *description = NULL;
11754 char *address = NULL;
11755 char *raw_image = NULL;
11756 char *image_url = NULL;
11757 char *current_logo_html = NULL;
11758 char *public_options_html = NULL;
11759 char *time_format_options_html = NULL;
11760 char *time_format = NULL;
11761 char *result = NULL;
11762 int is_public = 0;
11763 TextBufferRuntime buffer = {0};
11764
11765 if (state_path == NULL || state_path[0] == '\0' || business_slug == NULL || business_slug[0] == '\0') {
11766 return NULL;
11767 }
11768 state_doc = native_json_doc_load_file(state_path);
11769 root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
11770 business = root != NULL ? state_find_business_by_slug_runtime(root, business_slug) : NULL;
11771 if (business == NULL) {
11772 native_json_doc_free(state_doc);
11773 return NULL;
11774 }
11775 name = native_json_obj_get_string_dup(business, "name");
11776 description = native_json_obj_get_string_dup(business, "description");
11777 address = native_json_obj_get_string_dup(business, "address");
11778 raw_image = native_json_obj_get_string_dup(business, "image_url");
11779 image_url = business_media_url_dup_runtime(business_slug, raw_image != NULL ? raw_image : "");
11780 time_format = native_json_obj_get_string_dup(business, "time_format");
11781 is_public = json_value_truthy_runtime(native_json_obj_get(business, "is_public"));
11782 if (image_url != NULL && image_url[0] != '\0') {
11783 if (!text_buffer_append_text_runtime(&buffer, "<img src=\"")
11784 || !text_buffer_append_html_escaped_runtime(&buffer, image_url)
11785 || !text_buffer_append_text_runtime(&buffer, "\" alt=\"")
11786 || !text_buffer_append_html_escaped_runtime(&buffer, name != NULL ? name : "")
11787 || !text_buffer_append_text_runtime(&buffer, "\" style=\"max-width:180px;max-height:180px;border-radius:12px;border:1px solid #d0d7de;display:block;\">")) {
11788 goto business_edit_params_cleanup;
11789 }
11790 } else if (!text_buffer_append_text_runtime(&buffer, "<p class=\"muted\" style=\"margin:0;\">No logo uploaded.</p>")) {
11791 goto business_edit_params_cleanup;
11792 }
11793 current_logo_html = text_buffer_take_runtime(&buffer);
11794 if (current_logo_html == NULL) {
11795 goto business_edit_params_cleanup;
11796 }
11797 public_options_html = strdup(is_public ? "<option value=\"true\" selected>Public</option><option value=\"false\">Private</option>" : "<option value=\"true\">Public</option><option value=\"false\" selected>Private</option>");
11798 time_format_options_html = strdup(time_format != NULL && streq(time_format, "military") ? "<option value=\"am_pm\">12-hour (AM/PM)</option><option value=\"military\" selected>24-hour (Military)</option>" : "<option value=\"am_pm\" selected>12-hour (AM/PM)</option><option value=\"military\">24-hour (Military)</option>");
11799 doc = yyjson_mut_doc_new(NULL);
11800 page = doc != NULL ? yyjson_mut_obj(doc) : NULL;
11801 if (public_options_html == NULL || time_format_options_html == NULL || doc == NULL || page == NULL) {
11802 goto business_edit_params_cleanup;
11803 }
11804 yyjson_mut_doc_set_root(doc, page);
11805 {
11806 char save_path[256];
11807 char cancel_path[256];
11808 snprintf(save_path, sizeof(save_path), "/businesses/%s/edit/", business_slug);
11809 snprintf(cancel_path, sizeof(cancel_path), "/businesses/%s/", business_slug);
11810 if (!yyjson_mut_obj_add_strcpy(doc, page, "save_path", save_path)
11811 || !yyjson_mut_obj_add_strcpy(doc, page, "cancel_path", cancel_path)
11812 || !yyjson_mut_obj_add_strcpy(doc, page, "current_logo_html", current_logo_html != NULL ? current_logo_html : "")
11813 || !yyjson_mut_obj_add_strcpy(doc, page, "business_name", name != NULL ? name : "")
11814 || !yyjson_mut_obj_add_strcpy(doc, page, "business_description", description != NULL ? description : "")
11815 || !yyjson_mut_obj_add_strcpy(doc, page, "business_address", address != NULL ? address : "")
11816 || !yyjson_mut_obj_add_strcpy(doc, page, "public_options_html", public_options_html != NULL ? public_options_html : "")
11817 || !yyjson_mut_obj_add_strcpy(doc, page, "time_format_options_html", time_format_options_html != NULL ? time_format_options_html : "")) {
11818 goto business_edit_params_cleanup;
11819 }
11820 }
11821 result = yyjson_mut_write(doc, YYJSON_WRITE_NOFLAG, NULL);
11822
11823business_edit_params_cleanup:
11824 text_buffer_free_runtime(&buffer);
11825 free(name); free(description); free(address); free(raw_image); free(image_url); free(current_logo_html); free(public_options_html); free(time_format_options_html); free(time_format);
11827 native_json_doc_free(state_doc);
11828 return result;
11829}
11830
11831static int emit_service_edit_page_json_runtime(yyjson_val *root, const char *slug, long service_id, const char *app_root) {
11832 yyjson_val *business = state_find_business_by_slug_runtime(root, slug);
11833 yyjson_val *service = state_find_service_by_id_runtime(root, service_id);
11834 yyjson_mut_doc *doc = NULL;
11835 yyjson_mut_val *page = NULL;
11836 char *name = NULL;
11837 char *description = NULL;
11838 char *cost = NULL;
11839 char *image_raw = NULL;
11840 char *image_url = NULL;
11841 char *service_visual_html = NULL;
11842 char *service_sort_order_options_html = NULL;
11843 char *booking_options_editor_html = NULL;
11844 char *technician_checkboxes_html = NULL;
11845 long business_id = 0;
11846 long service_duration = 45;
11847 long service_sort_order = 0;
11848 int code = 69;
11849 if (business == NULL || service == NULL || slug == NULL || slug[0] == '\0') {
11850 return 0;
11851 }
11852 business_id = native_json_obj_get_long_default(business, "id", 0, NULL);
11853 if (!value_long_equals_runtime(native_json_obj_get(service, "business_id"), business_id)) {
11854 return 0;
11855 }
11856 name = native_json_obj_get_string_dup(service, "name");
11857 description = native_json_obj_get_string_dup(service, "description");
11858 cost = native_json_obj_get_string_dup(service, "cost");
11859 image_raw = native_json_obj_get_string_dup(service, "image_url");
11860 if (image_raw == NULL || image_raw[0] == '\0') {
11861 free(image_raw);
11862 image_raw = native_json_obj_get_string_dup(service, "image");
11863 }
11864 image_url = service_media_url_dup_runtime(slug, image_raw != NULL ? image_raw : "");
11865 service_duration = native_json_obj_get_long_default(service, "duration_minutes", 45, NULL);
11866 service_sort_order = native_json_obj_get_long_default(service, "sort_order", native_json_obj_get_long_default(service, "id", 1, NULL), NULL);
11867 if (service_duration <= 0) {
11868 service_duration = 45;
11869 }
11870 if (service_sort_order <= 0) {
11871 service_sort_order = native_json_obj_get_long_default(service, "id", 1, NULL);
11872 }
11873 service_visual_html = render_service_visual_html_dup_runtime(app_root, image_url != NULL ? image_url : "", name != NULL ? name : "");
11874 service_sort_order_options_html = render_service_sort_order_options_html_dup_runtime(root, business_id, service_sort_order, 0, app_root);
11875 booking_options_editor_html = render_service_booking_options_editor_html_dup_runtime(root, service_id, app_root);
11876 technician_checkboxes_html = render_technician_checkboxes_html_dup_runtime(root, business_id, service_id, "No technicians available for this business yet.", 0, app_root);
11877 doc = yyjson_mut_doc_new(NULL);
11878 page = doc != NULL ? yyjson_mut_obj(doc) : NULL;
11879 if (service_visual_html == NULL || service_sort_order_options_html == NULL || booking_options_editor_html == NULL || technician_checkboxes_html == NULL || doc == NULL || page == NULL) {
11880 goto service_edit_cleanup;
11881 }
11882 yyjson_mut_doc_set_root(doc, page);
11883 {
11884 char save_path[256];
11885 char cancel_path[256];
11886 char duration_buffer[32];
11887 snprintf(save_path, sizeof(save_path), "/businesses/%s/services/%ld/edit/", slug, service_id);
11888 snprintf(cancel_path, sizeof(cancel_path), "/businesses/%s/", slug);
11889 snprintf(duration_buffer, sizeof(duration_buffer), "%ld", service_duration);
11890 if (!yyjson_mut_obj_add_strcpy(doc, page, "save_path", save_path)
11891 || !yyjson_mut_obj_add_strcpy(doc, page, "cancel_path", cancel_path)
11892 || !yyjson_mut_obj_add_strcpy(doc, page, "service_visual_html", service_visual_html)
11893 || !yyjson_mut_obj_add_strcpy(doc, page, "service_name", name != NULL ? name : "")
11894 || !yyjson_mut_obj_add_strcpy(doc, page, "service_description", description != NULL ? description : "")
11895 || !yyjson_mut_obj_add_strcpy(doc, page, "service_cost", cost != NULL ? cost : "")
11896 || !yyjson_mut_obj_add_strcpy(doc, page, "service_duration", duration_buffer)
11897 || !yyjson_mut_obj_add_strcpy(doc, page, "service_sort_order_options_html", service_sort_order_options_html)
11898 || !yyjson_mut_obj_add_strcpy(doc, page, "booking_options_editor_html", booking_options_editor_html)
11899 || !yyjson_mut_obj_add_strcpy(doc, page, "technician_checkboxes_html", technician_checkboxes_html)) {
11900 goto service_edit_cleanup;
11901 }
11902 }
11904service_edit_cleanup:
11905 free(name); free(description); free(cost); free(image_raw); free(image_url); free(service_visual_html); free(service_sort_order_options_html); free(booking_options_editor_html); free(technician_checkboxes_html);
11907 return code;
11908}
11909
11910static int emit_technician_edit_page_json_runtime(yyjson_val *root, const char *slug, long technician_id) {
11911 yyjson_val *business = state_find_business_by_slug_runtime(root, slug);
11912 yyjson_val *technician = state_find_technician_by_id_runtime(root, technician_id);
11913 yyjson_mut_doc *doc = NULL;
11914 yyjson_mut_val *page = NULL;
11915 char *name = NULL;
11916 char *email = NULL;
11917 int code = 69;
11918 if (business == NULL || technician == NULL || slug == NULL || slug[0] == '\0') {
11919 return 0;
11920 }
11921 if (!value_long_equals_runtime(native_json_obj_get(technician, "business_id"), native_json_obj_get_long_default(business, "id", 0, NULL))) {
11922 return 0;
11923 }
11924 name = native_json_obj_get_string_dup(technician, "name");
11925 email = native_json_obj_get_string_dup(technician, "email");
11926 doc = yyjson_mut_doc_new(NULL);
11927 page = doc != NULL ? yyjson_mut_obj(doc) : NULL;
11928 if (doc == NULL || page == NULL) {
11929 goto technician_edit_cleanup;
11930 }
11931 yyjson_mut_doc_set_root(doc, page);
11932 {
11933 char save_path[256];
11934 char cancel_path[256];
11935 snprintf(save_path, sizeof(save_path), "/businesses/%s/technicians/%ld/edit/", slug, technician_id);
11936 snprintf(cancel_path, sizeof(cancel_path), "/businesses/%s/", slug);
11937 if (!yyjson_mut_obj_add_strcpy(doc, page, "save_path", save_path)
11938 || !yyjson_mut_obj_add_strcpy(doc, page, "cancel_path", cancel_path)
11939 || !yyjson_mut_obj_add_strcpy(doc, page, "technician_name", name != NULL ? name : "")
11940 || !yyjson_mut_obj_add_strcpy(doc, page, "technician_email", email != NULL ? email : "")) {
11941 goto technician_edit_cleanup;
11942 }
11943 }
11945technician_edit_cleanup:
11946 free(name); free(email);
11948 return code;
11949}
11950
11952 yyjson_val *businesses = state_array_runtime(root, "businesses");
11953 yyjson_arr_iter iter;
11954 yyjson_val *row = NULL;
11955 yyjson_mut_doc *mut_doc = NULL;
11956 yyjson_mut_val *array = NULL;
11957 int code = 69;
11958 if (businesses == NULL || !yyjson_arr_iter_init(businesses, &iter)) {
11959 fputs("[]", stdout);
11960 return 0;
11961 }
11962 mut_doc = yyjson_mut_doc_new(NULL);
11963 array = mut_doc != NULL ? yyjson_mut_arr(mut_doc) : NULL;
11964 if (mut_doc == NULL || array == NULL) {
11965 yyjson_mut_doc_free(mut_doc);
11966 return 69;
11967 }
11968 yyjson_mut_doc_set_root(mut_doc, array);
11969 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
11970 yyjson_mut_val *obj = NULL;
11971 yyjson_val *public_value = NULL;
11972 char *slug = NULL;
11973 char *name = NULL;
11974 char *description = NULL;
11975 char *image_url = NULL;
11976 long business_id = 0;
11977 if (!yyjson_is_obj(row)) {
11978 continue;
11979 }
11980 public_value = native_json_obj_get(row, "is_public");
11981 if (!(yyjson_is_bool(public_value) && yyjson_get_bool(public_value))) {
11982 continue;
11983 }
11984 slug = native_json_obj_get_string_dup(row, "slug");
11985 if (slug == NULL || slug[0] == '\0') {
11986 free(slug);
11987 continue;
11988 }
11989 business_id = native_json_obj_get_long_default(row, "id", 0, NULL);
11990 name = native_json_obj_get_string_dup(row, "name");
11991 description = native_json_obj_get_string_dup(row, "description");
11992 image_url = native_json_obj_get_string_dup(row, "image_url");
11993 obj = yyjson_mut_obj(mut_doc);
11994 if (obj == NULL
11995 || !yyjson_mut_obj_add_int(mut_doc, obj, "id", business_id)
11996 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "slug", slug)
11997 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "name", name != NULL ? name : "")
11998 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "description", description != NULL ? description : "")
11999 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "image_url", image_url != NULL ? image_url : "")
12000 || !yyjson_mut_arr_append(array, obj)) {
12001 free(slug);
12002 free(name);
12003 free(description);
12004 free(image_url);
12005 yyjson_mut_doc_free(mut_doc);
12006 return 69;
12007 }
12008 free(slug);
12009 free(name);
12010 free(description);
12011 free(image_url);
12012 }
12014 yyjson_mut_doc_free(mut_doc);
12015 return code;
12016}
12017
12018static int emit_user_appointments_json_runtime(yyjson_val *root, long user_id) {
12019 yyjson_val *appointments = state_array_runtime(root, "appointments");
12020 yyjson_arr_iter iter;
12021 yyjson_val *row = NULL;
12022 yyjson_mut_doc *mut_doc = NULL;
12023 yyjson_mut_val *array = NULL;
12024 int code = 69;
12025 if (appointments == NULL || !yyjson_arr_iter_init(appointments, &iter)) {
12026 fputs("[]", stdout);
12027 return 0;
12028 }
12029 mut_doc = yyjson_mut_doc_new(NULL);
12030 array = mut_doc != NULL ? yyjson_mut_arr(mut_doc) : NULL;
12031 if (mut_doc == NULL || array == NULL) {
12032 yyjson_mut_doc_free(mut_doc);
12033 return 69;
12034 }
12035 yyjson_mut_doc_set_root(mut_doc, array);
12036 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
12037 yyjson_mut_val *obj = NULL;
12038 yyjson_val *business = NULL;
12039 yyjson_val *service = NULL;
12040 yyjson_val *technician = NULL;
12041 long business_id = 0;
12042 long service_id = 0;
12043 long technician_id = 0;
12044 char *business_name = NULL;
12045 char *business_slug = NULL;
12046 char *service_name = NULL;
12047 char *technician_name = NULL;
12048 char *date = NULL;
12049 char *time = NULL;
12050 char *status = NULL;
12051 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "user_id"), user_id)) {
12052 continue;
12053 }
12054 business_id = native_json_obj_get_long_default(row, "business_id", 0, NULL);
12055 service_id = native_json_obj_get_long_default(row, "service_id", 0, NULL);
12056 technician_id = native_json_obj_get_long_default(row, "technician_id", 0, NULL);
12057 business = state_find_business_by_id_runtime(root, business_id);
12058 service = state_find_service_by_id_runtime(root, service_id);
12059 technician = state_find_technician_for_business_runtime(root, technician_id, business_id);
12060 business_name = business != NULL ? native_json_obj_get_string_dup(business, "name") : NULL;
12061 business_slug = business != NULL ? native_json_obj_get_string_dup(business, "slug") : NULL;
12062 service_name = service != NULL ? native_json_obj_get_string_dup(service, "name") : NULL;
12063 technician_name = technician != NULL ? native_json_obj_get_string_dup(technician, "name") : dup_range("", 0);
12064 date = native_json_obj_get_string_dup(row, "date");
12065 time = native_json_obj_get_string_dup(row, "time");
12066 status = native_json_obj_get_string_dup(row, "status");
12067 obj = yyjson_mut_obj(mut_doc);
12068 if (obj == NULL
12069 || !yyjson_mut_obj_add_int(mut_doc, obj, "id", native_json_obj_get_long_default(row, "id", 0, NULL))
12070 || !yyjson_mut_obj_add_int(mut_doc, obj, "business_id", business_id)
12071 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "business_name", business_name != NULL ? business_name : "")
12072 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "business_slug", business_slug != NULL ? business_slug : "")
12073 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "service_name", service_name != NULL ? service_name : "")
12074 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "technician_name", technician_name != NULL ? technician_name : "")
12075 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "date", date != NULL ? date : "")
12076 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "time", time != NULL ? time : "")
12077 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "status", status != NULL ? status : "")
12078 || !yyjson_mut_arr_append(array, obj)) {
12079 free(business_name);
12080 free(business_slug);
12081 free(service_name);
12082 free(technician_name);
12083 free(date);
12084 free(time);
12085 free(status);
12086 yyjson_mut_doc_free(mut_doc);
12087 return 69;
12088 }
12089 free(business_name);
12090 free(business_slug);
12091 free(service_name);
12092 free(technician_name);
12093 free(date);
12094 free(time);
12095 free(status);
12096 }
12098 yyjson_mut_doc_free(mut_doc);
12099 return code;
12100}
12101
12103 yyjson_val *appointments = state_array_runtime(root, "appointments");
12104 yyjson_arr_iter iter;
12105 yyjson_val *row = NULL;
12106 yyjson_mut_doc *mut_doc = NULL;
12107 yyjson_mut_val *array = NULL;
12108 int code = 69;
12109 if (appointments == NULL || !yyjson_arr_iter_init(appointments, &iter)) {
12110 fputs("[]", stdout);
12111 return 0;
12112 }
12113 mut_doc = yyjson_mut_doc_new(NULL);
12114 array = mut_doc != NULL ? yyjson_mut_arr(mut_doc) : NULL;
12115 if (mut_doc == NULL || array == NULL) {
12116 yyjson_mut_doc_free(mut_doc);
12117 return 69;
12118 }
12119 yyjson_mut_doc_set_root(mut_doc, array);
12120 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
12121 yyjson_mut_val *obj = NULL;
12122 yyjson_val *business = NULL;
12123 yyjson_val *service = NULL;
12124 yyjson_val *customer = NULL;
12125 yyjson_val *technician = NULL;
12126 long business_id = 0;
12127 long tenant_id = 0;
12128 long service_id = 0;
12129 long user_id = 0;
12130 long technician_id = 0;
12131 char *business_name = NULL;
12132 char *service_name = NULL;
12133 char *customer_name = NULL;
12134 char *technician_name = NULL;
12135 char *date = NULL;
12136 char *time = NULL;
12137 char *status = NULL;
12138 if (!yyjson_is_obj(row)) {
12139 continue;
12140 }
12141 business_id = native_json_obj_get_long_default(row, "business_id", 0, NULL);
12142 tenant_id = tenant_id_for_business_runtime(root, business_id);
12143 if (!json_array_contains_long_runtime(tenant_ids, tenant_id)) {
12144 continue;
12145 }
12146 service_id = native_json_obj_get_long_default(row, "service_id", 0, NULL);
12147 user_id = native_json_obj_get_long_default(row, "user_id", 0, NULL);
12148 technician_id = native_json_obj_get_long_default(row, "technician_id", 0, NULL);
12149 business = state_find_business_by_id_runtime(root, business_id);
12150 service = state_find_service_by_id_runtime(root, service_id);
12151 customer = state_find_user_by_id_runtime(root, user_id);
12152 technician = state_find_technician_for_business_runtime(root, technician_id, business_id);
12153 business_name = business != NULL ? native_json_obj_get_string_dup(business, "name") : NULL;
12154 service_name = service != NULL ? native_json_obj_get_string_dup(service, "name") : NULL;
12155 customer_name = customer != NULL ? native_json_obj_get_string_dup(customer, "full_name") : NULL;
12156 technician_name = technician != NULL ? native_json_obj_get_string_dup(technician, "name") : dup_range("", 0);
12157 date = native_json_obj_get_string_dup(row, "date");
12158 time = native_json_obj_get_string_dup(row, "time");
12159 status = native_json_obj_get_string_dup(row, "status");
12160 obj = yyjson_mut_obj(mut_doc);
12161 if (obj == NULL
12162 || !yyjson_mut_obj_add_int(mut_doc, obj, "id", native_json_obj_get_long_default(row, "id", 0, NULL))
12163 || !yyjson_mut_obj_add_int(mut_doc, obj, "business_id", business_id)
12164 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "business_name", business_name != NULL ? business_name : "")
12165 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "service_name", service_name != NULL ? service_name : "")
12166 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "customer_name", customer_name != NULL ? customer_name : "")
12167 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "technician_name", technician_name != NULL ? technician_name : "")
12168 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "date", date != NULL ? date : "")
12169 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "time", time != NULL ? time : "")
12170 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "status", status != NULL ? status : "")
12171 || !yyjson_mut_arr_append(array, obj)) {
12172 free(business_name);
12173 free(service_name);
12174 free(customer_name);
12175 free(technician_name);
12176 free(date);
12177 free(time);
12178 free(status);
12179 yyjson_mut_doc_free(mut_doc);
12180 return 69;
12181 }
12182 free(business_name);
12183 free(service_name);
12184 free(customer_name);
12185 free(technician_name);
12186 free(date);
12187 free(time);
12188 free(status);
12189 }
12191 yyjson_mut_doc_free(mut_doc);
12192 return code;
12193}
12194
12195static int emit_profile_page_json_runtime(yyjson_val *root, long user_id) {
12196 yyjson_val *user = state_find_user_by_id_runtime(root, user_id);
12197 yyjson_val *subscriptions = state_array_runtime(root, "subscriptions");
12198 yyjson_mut_doc *mut_doc = NULL;
12199 yyjson_mut_val *page_obj = NULL;
12200 yyjson_mut_val *subs_array = NULL;
12201 yyjson_arr_iter iter;
12202 yyjson_val *row = NULL;
12203 int code = 69;
12204 mut_doc = yyjson_mut_doc_new(NULL);
12205 page_obj = mut_doc != NULL ? yyjson_mut_obj(mut_doc) : NULL;
12206 subs_array = mut_doc != NULL ? yyjson_mut_arr(mut_doc) : NULL;
12207 if (mut_doc == NULL || page_obj == NULL || subs_array == NULL) {
12208 yyjson_mut_doc_free(mut_doc);
12209 return 69;
12210 }
12211 yyjson_mut_doc_set_root(mut_doc, page_obj);
12212 {
12213 char *username = user != NULL ? native_json_obj_get_string_dup(user, "username") : NULL;
12214 char *full_name = user != NULL ? native_json_obj_get_string_dup(user, "full_name") : NULL;
12215 char *email = user != NULL ? native_json_obj_get_string_dup(user, "email") : NULL;
12216 char *phone_number = user != NULL ? native_json_obj_get_string_dup(user, "phone_number") : NULL;
12217 char *address = user != NULL ? native_json_obj_get_string_dup(user, "address") : NULL;
12218 char *payment_preference = user != NULL ? native_json_obj_get_string_dup(user, "payment_preference") : NULL;
12219 if (!yyjson_mut_obj_add_int(mut_doc, page_obj, "id", user_id)
12220 || !yyjson_mut_obj_add_strcpy(mut_doc, page_obj, "username", username != NULL ? username : "")
12221 || !yyjson_mut_obj_add_strcpy(mut_doc, page_obj, "full_name", full_name != NULL ? full_name : "")
12222 || !yyjson_mut_obj_add_strcpy(mut_doc, page_obj, "email", email != NULL ? email : "")
12223 || !yyjson_mut_obj_add_strcpy(mut_doc, page_obj, "phone_number", phone_number != NULL ? phone_number : "")
12224 || !yyjson_mut_obj_add_strcpy(mut_doc, page_obj, "address", address != NULL ? address : "")
12225 || !yyjson_mut_obj_add_strcpy(mut_doc, page_obj, "payment_preference", payment_preference != NULL && payment_preference[0] != '\0' ? payment_preference : "pay_on_site")
12226 || !yyjson_mut_obj_put(page_obj, yyjson_mut_strcpy(mut_doc, "subscriptions"), subs_array)) {
12227 free(username);
12228 free(full_name);
12229 free(email);
12230 free(phone_number);
12231 free(address);
12232 free(payment_preference);
12233 yyjson_mut_doc_free(mut_doc);
12234 return 69;
12235 }
12236 free(username);
12237 free(full_name);
12238 free(email);
12239 free(phone_number);
12240 free(address);
12241 free(payment_preference);
12242 }
12243 if (subscriptions != NULL && yyjson_arr_iter_init(subscriptions, &iter)) {
12244 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
12245 yyjson_mut_val *obj = NULL;
12246 yyjson_val *business = NULL;
12247 long business_id = 0;
12248 char *business_name = NULL;
12249 char *business_slug = NULL;
12250 if (!yyjson_is_obj(row) || !value_long_equals_runtime(native_json_obj_get(row, "user_id"), user_id)) {
12251 continue;
12252 }
12253 business_id = native_json_obj_get_long_default(row, "business_id", 0, NULL);
12254 business = state_find_business_by_id_runtime(root, business_id);
12255 business_name = business != NULL ? native_json_obj_get_string_dup(business, "name") : NULL;
12256 business_slug = business != NULL ? native_json_obj_get_string_dup(business, "slug") : NULL;
12257 obj = yyjson_mut_obj(mut_doc);
12258 if (obj == NULL
12259 || !yyjson_mut_obj_add_int(mut_doc, obj, "business_id", business_id)
12260 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "business_name", business_name != NULL ? business_name : "")
12261 || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "business_slug", business_slug != NULL ? business_slug : "")
12262 || !yyjson_mut_arr_append(subs_array, obj)) {
12263 free(business_name);
12264 free(business_slug);
12265 yyjson_mut_doc_free(mut_doc);
12266 return 69;
12267 }
12268 free(business_name);
12269 free(business_slug);
12270 }
12271 }
12272 code = emit_compact_json_mut_value_runtime(page_obj);
12273 yyjson_mut_doc_free(mut_doc);
12274 return code;
12275}
12276
12277
12278
12279static yyjson_val *find_active_token_record_runtime(yyjson_val *array_value, const char *token_hash, const char *now_iso) {
12280 yyjson_arr_iter iter;
12281 yyjson_val *item = NULL;
12282 if (array_value == NULL || token_hash == NULL || token_hash[0] == '\0' || now_iso == NULL || now_iso[0] == '\0' || !yyjson_is_arr(array_value)) {
12283 return NULL;
12284 }
12285 if (!yyjson_arr_iter_init(array_value, &iter)) {
12286 return NULL;
12287 }
12288 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
12289 yyjson_val *hash_value = NULL;
12290 char *expires_at = NULL;
12291 char *claimed_at = NULL;
12292 int match = 0;
12293 if (!yyjson_is_obj(item)) {
12294 continue;
12295 }
12296 hash_value = native_json_obj_get(item, "token_hash");
12297 if (!yyjson_is_str(hash_value)) {
12298 continue;
12299 }
12300 match = yyjson_equals_str(hash_value, token_hash);
12301 expires_at = native_json_obj_get_string_dup(item, "expires_at");
12302 claimed_at = native_json_obj_get_string_dup(item, "claimed_at");
12303 if (match && (claimed_at == NULL || claimed_at[0] == '\0') && expires_at != NULL && strcmp(expires_at, now_iso) > 0) {
12304 free(expires_at);
12305 free(claimed_at);
12306 return item;
12307 }
12308 free(expires_at);
12309 free(claimed_at);
12310 }
12311 return NULL;
12312}
12313
12314typedef struct {
12316 size_t count;
12317 size_t capacity;
12319
12321 if (list != NULL) {
12322 free(list->items);
12323 list->items = NULL;
12324 list->count = 0;
12325 list->capacity = 0;
12326 }
12327}
12328
12330 yyjson_val **next_items = NULL;
12331 size_t next_capacity = 0;
12332 if (list == NULL || item == NULL) {
12333 return 0;
12334 }
12335 if (list->count < list->capacity) {
12336 list->items[list->count++] = item;
12337 return 1;
12338 }
12339 next_capacity = list->capacity == 0 ? 16 : list->capacity * 2;
12340 next_items = (yyjson_val **)realloc(list->items, next_capacity * sizeof(yyjson_val *));
12341 if (next_items == NULL) {
12342 return 0;
12343 }
12344 list->items = next_items;
12345 list->capacity = next_capacity;
12346 list->items[list->count++] = item;
12347 return 1;
12348}
12349
12350static int compare_item_id_runtime(const void *lhs_ptr, const void *rhs_ptr) {
12351 const yyjson_val *lhs = *(const yyjson_val * const *)lhs_ptr;
12352 const yyjson_val *rhs = *(const yyjson_val * const *)rhs_ptr;
12353 long lhs_id = native_json_obj_get_long_default((yyjson_val *)lhs, "id", 0, NULL);
12354 long rhs_id = native_json_obj_get_long_default((yyjson_val *)rhs, "id", 0, NULL);
12355 if (lhs_id < rhs_id) {
12356 return -1;
12357 }
12358 if (lhs_id > rhs_id) {
12359 return 1;
12360 }
12361 return 0;
12362}
12363
12365 char *text = NULL;
12366 if (value == NULL) {
12367 return 69;
12368 }
12370 if (text == NULL) {
12371 return 69;
12372 }
12373 fputs(text, stdout);
12374 fputc('\n', stdout);
12375 free(text);
12376 return 0;
12377}
12378
12379static int emit_sorted_section_lines_runtime(yyjson_val *root, const char *section_name) {
12380 yyjson_val *array = state_array_runtime(root, section_name);
12381 yyjson_arr_iter iter;
12382 yyjson_val *item = NULL;
12383 json_value_list_runtime list = {0};
12384 size_t i = 0;
12385 int code = 0;
12386 if (array == NULL || !yyjson_arr_iter_init(array, &iter)) {
12387 return 0;
12388 }
12389 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
12390 if (!yyjson_is_obj(item) || !json_value_list_append_runtime(&list, item)) {
12392 return 69;
12393 }
12394 }
12395 qsort(list.items, list.count, sizeof(yyjson_val *), compare_item_id_runtime);
12396 for (i = 0; i < list.count; i++) {
12397 code = emit_json_line_runtime(list.items[i]);
12398 if (code != 0) {
12400 return code;
12401 }
12402 }
12404 return 0;
12405}
12406
12407static int seen_long_pair_runtime(long *values, size_t count, long first, long second) {
12408 size_t i = 0;
12409 for (i = 0; i + 1 < count; i += 2) {
12410 if (values[i] == first && values[i + 1] == second) {
12411 return 1;
12412 }
12413 }
12414 return 0;
12415}
12416
12417static int handle_state_ucal_import_row_json_lines_command(yyjson_val *root, const char *section_name) {
12418 if (section_name == NULL || section_name[0] == '\0') {
12419 return 64;
12420 }
12421 if (streq(section_name, "users") || streq(section_name, "sessions") || streq(section_name, "businesses") || streq(section_name, "technicians") || streq(section_name, "business_availability") || streq(section_name, "technician_availability") || streq(section_name, "services") || streq(section_name, "bookings") || streq(section_name, "appointments") || streq(section_name, "email_verifications") || streq(section_name, "password_resets") || streq(section_name, "invites") || streq(section_name, "rate_limit_events") || streq(section_name, "workflow_executions") || streq(section_name, "workflow_execution_events") || streq(section_name, "job_executions") || streq(section_name, "job_execution_attempts")) {
12422 return emit_sorted_section_lines_runtime(root, section_name);
12423 }
12424 if (streq(section_name, "tenants")) {
12425 yyjson_val *tenants = state_array_runtime(root, "tenants");
12426 if (tenants != NULL && yyjson_arr_size(tenants) > 0) {
12427 return emit_sorted_section_lines_runtime(root, "tenants");
12428 }
12429 {
12430 yyjson_val *businesses = state_array_runtime(root, "businesses");
12431 yyjson_arr_iter iter;
12432 yyjson_val *item = NULL;
12433 json_value_list_runtime list = {0};
12434 size_t i = 0;
12435 if (businesses == NULL || !yyjson_arr_iter_init(businesses, &iter)) {
12436 return 0;
12437 }
12438 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
12439 if (yyjson_is_obj(item) && !json_value_list_append_runtime(&list, item)) {
12441 return 69;
12442 }
12443 }
12444 qsort(list.items, list.count, sizeof(yyjson_val *), compare_item_id_runtime);
12445 for (i = 0; i < list.count; i++) {
12446 yyjson_mut_doc *mut_doc = yyjson_mut_doc_new(NULL);
12447 yyjson_mut_val *obj = yyjson_mut_obj(mut_doc);
12448 char *slug = native_json_obj_get_string_dup(list.items[i], "slug");
12449 char *name = native_json_obj_get_string_dup(list.items[i], "name");
12450 long id = native_json_obj_get_long_default(list.items[i], "id", 0, NULL);
12451 long owner_id = native_json_obj_get_long_default(list.items[i], "owner_id", 0, NULL);
12452 if (mut_doc == NULL || obj == NULL || !yyjson_mut_obj_add_int(mut_doc, obj, "id", id) || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "tenant_kind", "business") || !yyjson_mut_obj_add_int(mut_doc, obj, "parent_tenant_id", 0) || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "slug", slug != NULL ? slug : "") || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "display_name", name != NULL ? name : "") || !yyjson_mut_obj_add_bool(mut_doc, obj, "is_active", 1) || !yyjson_mut_obj_add_int(mut_doc, obj, "owner_user_id", owner_id) || !yyjson_mut_obj_add_int(mut_doc, obj, "source_business_id", id)) {
12453 free(slug);
12454 free(name);
12455 yyjson_mut_doc_free(mut_doc);
12457 return 69;
12458 }
12460 free(slug);
12461 free(name);
12462 yyjson_mut_doc_free(mut_doc);
12463 }
12465 return 0;
12466 }
12467 }
12468 if (streq(section_name, "tenant_memberships")) {
12469 yyjson_val *memberships = state_array_runtime(root, "tenant_memberships");
12470 if (memberships != NULL && yyjson_arr_size(memberships) > 0) {
12471 return emit_sorted_section_lines_runtime(root, "tenant_memberships");
12472 }
12473 {
12474 yyjson_val *businesses = state_array_runtime(root, "businesses");
12475 yyjson_arr_iter iter;
12476 yyjson_val *item = NULL;
12477 json_value_list_runtime list = {0};
12478 size_t i = 0;
12479 if (businesses == NULL || !yyjson_arr_iter_init(businesses, &iter)) {
12480 return 0;
12481 }
12482 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
12483 if (yyjson_is_obj(item) && !json_value_list_append_runtime(&list, item)) {
12485 return 69;
12486 }
12487 }
12488 qsort(list.items, list.count, sizeof(yyjson_val *), compare_item_id_runtime);
12489 for (i = 0; i < list.count; i++) {
12490 yyjson_mut_doc *mut_doc = yyjson_mut_doc_new(NULL);
12491 yyjson_mut_val *obj = yyjson_mut_obj(mut_doc);
12492 long tenant_id = native_json_obj_get_long_default(list.items[i], "id", 0, NULL);
12493 long user_id = native_json_obj_get_long_default(list.items[i], "owner_id", 0, NULL);
12494 if (mut_doc == NULL || obj == NULL || !yyjson_mut_obj_add_int(mut_doc, obj, "id", (long)(i + 1)) || !yyjson_mut_obj_add_int(mut_doc, obj, "tenant_id", tenant_id) || !yyjson_mut_obj_add_int(mut_doc, obj, "user_id", user_id) || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "role_id", "business_owner") || !yyjson_mut_obj_add_strcpy(mut_doc, obj, "membership_status", "active") || !yyjson_mut_obj_add_int(mut_doc, obj, "invited_by_user_id", 0)) {
12495 yyjson_mut_doc_free(mut_doc);
12497 return 69;
12498 }
12500 yyjson_mut_doc_free(mut_doc);
12501 }
12503 return 0;
12504 }
12505 }
12506 if (streq(section_name, "subscriptions") || streq(section_name, "ratings")) {
12507 yyjson_val *array = state_array_runtime(root, section_name);
12508 yyjson_arr_iter iter;
12509 yyjson_val *item = NULL;
12510 json_value_list_runtime list = {0};
12511 long seen_pairs[2048];
12512 size_t seen_count = 0;
12513 size_t idx = 0;
12514 if (array == NULL || !yyjson_arr_iter_init(array, &iter)) {
12515 return 0;
12516 }
12517 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
12518 if (yyjson_is_obj(item) && !json_value_list_append_runtime(&list, item)) {
12520 return 69;
12521 }
12522 }
12523 for (idx = list.count; idx > 0; idx--) {
12524 yyjson_val *candidate = list.items[idx - 1];
12525 long user_id = native_json_obj_get_long_default(candidate, "user_id", 0, NULL);
12526 long business_id = native_json_obj_get_long_default(candidate, "business_id", 0, NULL);
12527 if (seen_count + 2 <= sizeof(seen_pairs) / sizeof(seen_pairs[0]) && !seen_long_pair_runtime(seen_pairs, seen_count, user_id, business_id)) {
12528 seen_pairs[seen_count++] = user_id;
12529 seen_pairs[seen_count++] = business_id;
12530 list.items[idx - 1] = candidate;
12531 } else {
12532 list.items[idx - 1] = NULL;
12533 }
12534 }
12535 qsort(list.items, list.count, sizeof(yyjson_val *), compare_item_id_runtime);
12536 for (idx = 0; idx < list.count; idx++) {
12537 if (list.items[idx] != NULL) {
12538 emit_json_line_runtime(list.items[idx]);
12539 }
12540 }
12542 return 0;
12543 }
12544 if (streq(section_name, "google_calendar_connections")) {
12545 yyjson_val *array = state_array_runtime(root, "google_calendar_connections");
12546 yyjson_arr_iter iter;
12547 yyjson_val *item = NULL;
12548 json_value_list_runtime list = {0};
12549 size_t idx = 0;
12550 if (array == NULL || !yyjson_arr_iter_init(array, &iter)) {
12551 return 0;
12552 }
12553 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
12554 if (yyjson_is_obj(item) && !json_value_list_append_runtime(&list, item)) {
12556 return 69;
12557 }
12558 }
12559 for (idx = list.count; idx > 0; idx--) {
12560 yyjson_val *candidate = list.items[idx - 1];
12561 size_t j = 0;
12562 long user_id = native_json_obj_get_long_default(candidate, "user_id", 0, NULL);
12563 long business_id = native_json_obj_get_long_default(candidate, "business_id", 0, NULL);
12564 char *mode = native_json_obj_get_string_dup(candidate, "mode");
12565 int seen = 0;
12566 for (j = idx; j < list.count; j++) {
12567 yyjson_val *other = list.items[j];
12568 char *other_mode = NULL;
12569 if (other == NULL) {
12570 continue;
12571 }
12572 other_mode = native_json_obj_get_string_dup(other, "mode");
12573 if (native_json_obj_get_long_default(other, "user_id", 0, NULL) == user_id && native_json_obj_get_long_default(other, "business_id", 0, NULL) == business_id && ((mode == NULL && other_mode == NULL) || (mode != NULL && other_mode != NULL && streq(mode, other_mode)))) {
12574 seen = 1;
12575 }
12576 free(other_mode);
12577 if (seen) {
12578 break;
12579 }
12580 }
12581 free(mode);
12582 if (seen) {
12583 list.items[idx - 1] = NULL;
12584 }
12585 }
12586 qsort(list.items, list.count, sizeof(yyjson_val *), compare_item_id_runtime);
12587 for (idx = 0; idx < list.count; idx++) {
12588 if (list.items[idx] != NULL) {
12589 emit_json_line_runtime(list.items[idx]);
12590 }
12591 }
12593 return 0;
12594 }
12595 if (streq(section_name, "audit_log")) {
12596 yyjson_val *array = state_array_runtime(root, "audit_log");
12597 yyjson_arr_iter iter;
12598 yyjson_val *item = NULL;
12599 if (array != NULL && yyjson_arr_iter_init(array, &iter)) {
12600 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
12602 }
12603 }
12604 return 0;
12605 }
12606 return 64;
12607}
12608
12622
12628
12630 if (row == NULL) {
12631 return;
12632 }
12633 free(row->source);
12634 free(row->event_kind);
12635 free(row->created_at);
12636 free(row->resource_kind);
12637 free(row->resource_id);
12638 free(row->replay_trace_id);
12639 memset(row, 0, sizeof(*row));
12640}
12641
12643 size_t index = 0;
12644 if (list == NULL) {
12645 return;
12646 }
12647 for (index = 0; index < list->count; index++) {
12648 free_event_stream_row_runtime(&list->items[index]);
12649 }
12650 free(list->items);
12651 list->items = NULL;
12652 list->count = 0;
12653 list->capacity = 0;
12654}
12655
12657 event_stream_row_runtime *next_items = NULL;
12658 size_t next_capacity = 0;
12659 if (list == NULL || row == NULL) {
12660 return 0;
12661 }
12662 if (list->count == list->capacity) {
12663 next_capacity = list->capacity == 0 ? 16 : list->capacity * 2;
12664 next_items = (event_stream_row_runtime *)realloc(list->items, next_capacity * sizeof(event_stream_row_runtime));
12665 if (next_items == NULL) {
12666 return 0;
12667 }
12668 list->items = next_items;
12669 list->capacity = next_capacity;
12670 }
12671 list->items[list->count++] = *row;
12672 memset(row, 0, sizeof(*row));
12673 return 1;
12674}
12675
12676static char *dup_or_default_runtime(const char *text, const char *fallback) {
12677 const char *source = text;
12678 if (source == NULL || source[0] == '\0') {
12679 source = fallback != NULL ? fallback : "";
12680 }
12681 return strdup(source);
12682}
12683
12684static long payload_first_long_runtime(yyjson_val *payload, const char **fields, size_t field_count) {
12685 size_t index = 0;
12686 if (payload == NULL || !yyjson_is_obj(payload)) {
12687 return 0;
12688 }
12689 for (index = 0; index < field_count; index++) {
12690 long value = 0;
12691 yyjson_val *field_value = native_json_obj_get(payload, fields[index]);
12692 if (long_from_value_runtime(field_value, &value)) {
12693 return value;
12694 }
12695 }
12696 return 0;
12697}
12698
12699static char *payload_first_string_dup_runtime(yyjson_val *payload, const char **fields, size_t field_count) {
12700 size_t index = 0;
12701 if (payload == NULL || !yyjson_is_obj(payload)) {
12702 return NULL;
12703 }
12704 for (index = 0; index < field_count; index++) {
12705 char *value = native_json_obj_get_string_dup(payload, fields[index]);
12706 if (value != NULL && value[0] != '\0') {
12707 return value;
12708 }
12709 free(value);
12710 }
12711 return NULL;
12712}
12713
12714static int long_in_array_runtime(yyjson_val *array_value, long expected_value) {
12715 yyjson_arr_iter iter;
12716 yyjson_val *item = NULL;
12717 if (array_value == NULL || !yyjson_is_arr(array_value) || !yyjson_arr_iter_init(array_value, &iter)) {
12718 return 0;
12719 }
12720 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
12721 long actual_value = 0;
12722 if (long_from_value_runtime(item, &actual_value) && actual_value == expected_value) {
12723 return 1;
12724 }
12725 }
12726 return 0;
12727}
12728
12729static char *resource_kind_from_resource_id_runtime(const char *resource_id) {
12730 const char *colon = NULL;
12731 if (resource_id == NULL || resource_id[0] == '\0') {
12732 return NULL;
12733 }
12734 colon = strchr(resource_id, ':');
12735 if (colon == NULL || colon == resource_id) {
12736 return NULL;
12737 }
12738 return dup_range(resource_id, (size_t)(colon - resource_id));
12739}
12740
12742 char *resource_id = NULL;
12743 char *entity_type = NULL;
12744 yyjson_val *entity_id_value = NULL;
12745 if (payload == NULL || !yyjson_is_obj(payload)) {
12746 return NULL;
12747 }
12748 resource_id = native_json_obj_get_string_dup(payload, "resource_id");
12749 if (resource_id != NULL && resource_id[0] != '\0') {
12750 return resource_id;
12751 }
12752 free(resource_id);
12753 entity_type = native_json_obj_get_string_dup(payload, "entity_type");
12754 entity_id_value = native_json_obj_get(payload, "entity_id");
12755 if (entity_type != NULL && entity_type[0] != '\0' && entity_id_value != NULL && !yyjson_is_null(entity_id_value)) {
12756 char *entity_id_json = native_json_serialize_compact_dup(entity_id_value);
12757 char *joined = NULL;
12758 if (entity_id_json != NULL) {
12759 size_t needed = strlen(entity_type) + strlen(entity_id_json) + 2;
12760 joined = (char *)malloc(needed);
12761 if (joined != NULL) {
12762 snprintf(joined, needed, "%s:%s", entity_type, entity_id_json);
12763 }
12764 }
12765 free(entity_type);
12766 free(entity_id_json);
12767 return joined;
12768 }
12769 free(entity_type);
12770 return NULL;
12771}
12772
12773static long event_stream_tenant_id_runtime(yyjson_val *root, yyjson_val *payload, long business_id) {
12774 const char *fields[] = {"tenant_id", "target_tenant_id"};
12775 long tenant_id = payload_first_long_runtime(payload, fields, sizeof(fields) / sizeof(fields[0]));
12776 if (tenant_id != 0) {
12777 return tenant_id;
12778 }
12779 if (business_id != 0) {
12780 return state_tenant_id_for_business_id_runtime(root, business_id);
12781 }
12782 return 0;
12783}
12784
12785static int add_json_string_or_null_runtime(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, const char *value) {
12786 if (doc == NULL || obj == NULL || key == NULL || key[0] == '\0') {
12787 return 0;
12788 }
12789 if (value == NULL) {
12790 return yyjson_mut_obj_put(obj, yyjson_mut_strcpy(doc, key), yyjson_mut_null(doc));
12791 }
12792 return yyjson_mut_obj_add_strcpy(doc, obj, key, value);
12793}
12794
12795static int compare_event_stream_rows_runtime(const void *lhs_ptr, const void *rhs_ptr) {
12796 const event_stream_row_runtime *lhs = (const event_stream_row_runtime *)lhs_ptr;
12797 const event_stream_row_runtime *rhs = (const event_stream_row_runtime *)rhs_ptr;
12798 const char *lhs_created_at = lhs != NULL && lhs->created_at != NULL ? lhs->created_at : "";
12799 const char *rhs_created_at = rhs != NULL && rhs->created_at != NULL ? rhs->created_at : "";
12800 const char *lhs_source = lhs != NULL && lhs->source != NULL ? lhs->source : "";
12801 const char *rhs_source = rhs != NULL && rhs->source != NULL ? rhs->source : "";
12802 int cmp = strcmp(lhs_created_at, rhs_created_at);
12803 if (cmp != 0) {
12804 return cmp;
12805 }
12806 cmp = strcmp(lhs_source, rhs_source);
12807 if (cmp != 0) {
12808 return cmp;
12809 }
12810 if (lhs->native_order < rhs->native_order) {
12811 return -1;
12812 }
12813 if (lhs->native_order > rhs->native_order) {
12814 return 1;
12815 }
12816 return 0;
12817}
12818
12819static int event_stream_allowed_by_scope_runtime(yyjson_val *subscription_scope, long event_user_id, long event_tenant_id) {
12820 char *scope_kind = NULL;
12821 long subscription_user_id = 0;
12822 yyjson_val *allowed_tenant_ids = NULL;
12823 int allowed = 0;
12824 if (subscription_scope == NULL || !yyjson_is_obj(subscription_scope)) {
12825 return 1;
12826 }
12827 scope_kind = native_json_obj_get_string_dup(subscription_scope, "scope_kind");
12828 if (scope_kind == NULL || scope_kind[0] == '\0' || streq(scope_kind, "all")) {
12829 free(scope_kind);
12830 return 1;
12831 }
12832 free(scope_kind);
12833 subscription_user_id = native_json_obj_get_long_default(subscription_scope, "user_id", 0, NULL);
12834 allowed_tenant_ids = native_json_obj_get(subscription_scope, "allowed_tenant_ids");
12835 allowed = (subscription_user_id != 0 && event_user_id == subscription_user_id) || (event_tenant_id != 0 && long_in_array_runtime(allowed_tenant_ids, event_tenant_id));
12836 return allowed;
12837}
12838
12840 char *source_filter = NULL;
12841 char *event_kind_filter = NULL;
12842 char *resource_id_filter = NULL;
12843 char *trace_id_filter = NULL;
12844 int matches = 1;
12845 if (filters == NULL || !yyjson_is_obj(filters) || row == NULL) {
12846 return 1;
12847 }
12848 source_filter = native_json_obj_get_string_dup(filters, "source");
12849 event_kind_filter = native_json_obj_get_string_dup(filters, "event_kind");
12850 resource_id_filter = native_json_obj_get_string_dup(filters, "resource_id");
12851 trace_id_filter = native_json_obj_get_string_dup(filters, "trace_id");
12852 if (source_filter != NULL && source_filter[0] != '\0' && !streq(source_filter, row->source != NULL ? row->source : "")) {
12853 matches = 0;
12854 }
12855 if (matches && event_kind_filter != NULL && event_kind_filter[0] != '\0' && !streq(event_kind_filter, row->event_kind != NULL ? row->event_kind : "")) {
12856 matches = 0;
12857 }
12858 if (matches && resource_id_filter != NULL && resource_id_filter[0] != '\0' && !streq(resource_id_filter, row->resource_id != NULL ? row->resource_id : "")) {
12859 matches = 0;
12860 }
12861 if (matches && trace_id_filter != NULL && trace_id_filter[0] != '\0' && !streq(trace_id_filter, row->replay_trace_id != NULL ? row->replay_trace_id : "")) {
12862 matches = 0;
12863 }
12864 free(source_filter);
12865 free(event_kind_filter);
12866 free(resource_id_filter);
12867 free(trace_id_filter);
12868 return matches;
12869}
12870
12871static int collect_event_stream_row_runtime(event_stream_row_list_runtime *rows, yyjson_val *root, const char *source, const char *event_kind_default, yyjson_val *payload, const char *created_at_field_fallback, long native_order, yyjson_val *subscription_scope, yyjson_val *filters, const char *created_at_override, const char *event_kind_override) {
12872 static const char *user_fields[] = {"user_id", "actor_user_id", "invited_by_user_id", "owner_user_id"};
12873 static const char *business_fields[] = {"business_id", "source_business_id"};
12874 static const char *tenant_fields[] = {"tenant_id", "target_tenant_id"};
12875 static const char *trace_fields[] = {"replay_trace_id", "trace_id"};
12877 char *created_at = NULL;
12878 char *event_kind = NULL;
12879 long business_id = 0;
12880 memset(&row, 0, sizeof(row));
12881 if (rows == NULL || root == NULL || source == NULL || payload == NULL || !yyjson_is_obj(payload)) {
12882 return 0;
12883 }
12884 row.source = dup_or_default_runtime(source, "");
12885 event_kind = event_kind_override != NULL ? strdup(event_kind_override) : NULL;
12886 if (event_kind == NULL || event_kind[0] == '\0') {
12887 free(event_kind);
12888 event_kind = native_json_obj_get_string_dup(payload, "event");
12889 }
12890 if (event_kind == NULL || event_kind[0] == '\0') {
12891 free(event_kind);
12892 event_kind = native_json_obj_get_string_dup(payload, "event_kind");
12893 }
12894 row.event_kind = event_kind != NULL && event_kind[0] != '\0' ? event_kind : dup_or_default_runtime(event_kind_default, "message");
12895 if (row.event_kind != event_kind) {
12896 free(event_kind);
12897 }
12898 created_at = created_at_override != NULL ? strdup(created_at_override) : NULL;
12899 if (created_at == NULL || created_at[0] == '\0') {
12900 free(created_at);
12901 created_at = created_at_field_fallback != NULL ? native_json_obj_get_string_dup(payload, created_at_field_fallback) : NULL;
12902 }
12903 row.created_at = created_at;
12904 row.native_order = native_order;
12905 row.user_id = payload_first_long_runtime(payload, user_fields, sizeof(user_fields) / sizeof(user_fields[0]));
12906 business_id = payload_first_long_runtime(payload, business_fields, sizeof(business_fields) / sizeof(business_fields[0]));
12907 row.business_id = business_id;
12908 row.tenant_id = payload_first_long_runtime(payload, tenant_fields, sizeof(tenant_fields) / sizeof(tenant_fields[0]));
12909 if (row.tenant_id == 0) {
12910 row.tenant_id = event_stream_tenant_id_runtime(root, payload, business_id);
12911 }
12913 row.resource_kind = payload_first_string_dup_runtime(payload, (const char *[]){"resource_kind", "entity_type"}, 2);
12914 if (row.resource_kind == NULL) {
12916 }
12917 row.replay_trace_id = payload_first_string_dup_runtime(payload, trace_fields, sizeof(trace_fields) / sizeof(trace_fields[0]));
12918 row.payload_json = payload;
12919 if (!event_stream_allowed_by_scope_runtime(subscription_scope, row.user_id, row.tenant_id) || !event_stream_matches_filters_runtime(filters, &row)) {
12921 return 1;
12922 }
12923 if (!append_event_stream_row_runtime(rows, &row)) {
12925 return 0;
12926 }
12927 return 1;
12928}
12929
12930static int handle_state_event_stream_row_json_lines_command(yyjson_val *root, yyjson_val *subscription_scope, yyjson_val *filters, long since_id, long limit_value) {
12932 yyjson_val *audit_log = state_array_runtime(root, "audit_log");
12933 yyjson_val *workflow_events = state_array_runtime(root, "workflow_execution_events");
12934 yyjson_val *job_executions = state_array_runtime(root, "job_executions");
12935 yyjson_val *job_execution_attempts = state_array_runtime(root, "job_execution_attempts");
12936 yyjson_arr_iter iter;
12937 yyjson_val *item = NULL;
12938 size_t index = 0;
12939 size_t emitted = 0;
12940 if (limit_value <= 0) {
12941 return 0;
12942 }
12943 if (audit_log != NULL && yyjson_arr_iter_init(audit_log, &iter)) {
12944 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
12945 long native_order = (long)(++index);
12946 char *created_at = native_json_obj_get_string_dup(item, "created_at");
12947 char *event_kind = native_json_obj_get_string_dup(item, "event");
12948 int ok = collect_event_stream_row_runtime(&rows, root, "audit_log", "audit_event", item, NULL, native_order, subscription_scope, filters, created_at, event_kind);
12949 free(created_at);
12950 free(event_kind);
12951 if (!ok) {
12953 return 69;
12954 }
12955 }
12956 }
12957 if (workflow_events != NULL && yyjson_arr_iter_init(workflow_events, &iter)) {
12958 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
12959 yyjson_val *payload = native_json_obj_get(item, "payload_json");
12960 char *created_at = native_json_obj_get_string_dup(item, "created_at");
12961 char *event_kind = native_json_obj_get_string_dup(item, "event_kind");
12962 long native_order = native_json_obj_get_long_default(item, "id", 0, NULL);
12963 int ok = collect_event_stream_row_runtime(&rows, root, "workflow_execution_event", "workflow_execution_event", payload != NULL && yyjson_is_obj(payload) ? payload : item, NULL, native_order, subscription_scope, filters, created_at, event_kind);
12964 free(created_at);
12965 free(event_kind);
12966 if (!ok) {
12968 return 69;
12969 }
12970 }
12971 }
12972 if (job_executions != NULL && yyjson_arr_iter_init(job_executions, &iter)) {
12973 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
12974 char *created_at = native_json_obj_get_string_dup(item, "updated_at");
12975 char *event_kind = native_json_obj_get_string_dup(item, "status");
12976 long native_order = native_json_obj_get_long_default(item, "id", 0, NULL);
12977 if (created_at == NULL || created_at[0] == '\0') {
12978 free(created_at);
12979 created_at = native_json_obj_get_string_dup(item, "created_at");
12980 }
12981 if (!collect_event_stream_row_runtime(&rows, root, "job_execution", "job_execution", item, NULL, native_order, subscription_scope, filters, created_at, event_kind)) {
12982 free(created_at);
12983 free(event_kind);
12985 return 69;
12986 }
12987 free(created_at);
12988 free(event_kind);
12989 }
12990 }
12991 if (job_execution_attempts != NULL && yyjson_arr_iter_init(job_execution_attempts, &iter)) {
12992 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
12993 char *created_at = native_json_obj_get_string_dup(item, "completed_at");
12994 char *event_kind = native_json_obj_get_string_dup(item, "status");
12995 long native_order = native_json_obj_get_long_default(item, "id", 0, NULL);
12996 if (created_at == NULL || created_at[0] == '\0') {
12997 free(created_at);
12998 created_at = native_json_obj_get_string_dup(item, "started_at");
12999 }
13000 if (created_at == NULL || created_at[0] == '\0') {
13001 free(created_at);
13002 created_at = native_json_obj_get_string_dup(item, "created_at");
13003 }
13004 if (!collect_event_stream_row_runtime(&rows, root, "job_execution_attempt", "job_execution_attempt", item, NULL, native_order, subscription_scope, filters, created_at, event_kind)) {
13005 free(created_at);
13006 free(event_kind);
13008 return 69;
13009 }
13010 free(created_at);
13011 free(event_kind);
13012 }
13013 }
13015 for (index = 0; index < rows.count; index++) {
13016 yyjson_mut_doc *doc = NULL;
13017 yyjson_mut_val *obj = NULL;
13018 long stream_id = (long)index + 1;
13019 int code = 0;
13020 if (stream_id <= since_id) {
13021 continue;
13022 }
13023 if (emitted >= (size_t)limit_value) {
13024 break;
13025 }
13026 doc = yyjson_mut_doc_new(NULL);
13027 obj = doc != NULL ? yyjson_mut_obj(doc) : NULL;
13028 if (doc == NULL || obj == NULL || !yyjson_mut_obj_add_int(doc, obj, "stream_id", stream_id) || !yyjson_mut_obj_add_strcpy(doc, obj, "source", rows.items[index].source != NULL ? rows.items[index].source : "") || !yyjson_mut_obj_add_strcpy(doc, obj, "event_kind", rows.items[index].event_kind != NULL ? rows.items[index].event_kind : "message") || !add_json_string_or_null_runtime(doc, obj, "created_at", rows.items[index].created_at) || !yyjson_mut_obj_add_int(doc, obj, "user_id", rows.items[index].user_id) || !yyjson_mut_obj_add_int(doc, obj, "business_id", rows.items[index].business_id) || !yyjson_mut_obj_add_int(doc, obj, "tenant_id", rows.items[index].tenant_id) || !add_json_string_or_null_runtime(doc, obj, "resource_kind", rows.items[index].resource_kind) || !add_json_string_or_null_runtime(doc, obj, "resource_id", rows.items[index].resource_id) || !add_json_string_or_null_runtime(doc, obj, "replay_trace_id", rows.items[index].replay_trace_id) || !yyjson_mut_obj_put(obj, yyjson_mut_strcpy(doc, "payload_json"), yyjson_val_mut_copy(doc, rows.items[index].payload_json))) {
13031 return 69;
13032 }
13033 yyjson_mut_doc_set_root(doc, obj);
13036 if (code != 0) {
13038 return code;
13039 }
13040 fputc('\n', stdout);
13041 emitted++;
13042 }
13044 return 0;
13045}
13046
13047static int handle_state_named_command(int argc, char **argv) {
13048 const char *name = NULL;
13049 const char *state_path = json_option_value_runtime(argc, argv, "--state");
13050 yyjson_doc *doc = NULL;
13051 yyjson_val *root = NULL;
13052 if (argc < 1 || argv == NULL || state_path == NULL || state_path[0] == '\0') {
13053 return 64;
13054 }
13055 name = argv[0];
13056 doc = native_json_doc_load_file(state_path);
13057 if (doc == NULL) {
13058 return 66;
13059 }
13060 root = yyjson_doc_get_root(doc);
13061 if (root == NULL || !yyjson_is_obj(root)) {
13063 return 66;
13064 }
13065 if (streq(name, "ucal-import-row-json-lines")) {
13066 const char *section_name = json_option_value_runtime(argc, argv, "--section");
13067 int code = handle_state_ucal_import_row_json_lines_command(root, section_name);
13069 return code;
13070 }
13071 if (streq(name, "business-json-by-slug")) {
13072 const char *slug = json_option_value_runtime(argc, argv, "--slug");
13075 return code;
13076 }
13077 if (streq(name, "business-id-by-slug")) {
13078 const char *slug = json_option_value_runtime(argc, argv, "--slug");
13079 yyjson_val *business = state_find_business_by_slug_runtime(root, slug);
13080 int code = emit_long_runtime(business != NULL ? native_json_obj_get_long_default(business, "id", 0, NULL) : 0);
13082 return code;
13083 }
13084 if (streq(name, "tenant-id-for-business-slug")) {
13085 const char *slug = json_option_value_runtime(argc, argv, "--slug");
13088 return code;
13089 }
13090 if (streq(name, "tenant-id-for-service-id")) {
13091 long service_id = strtol(json_option_value_runtime(argc, argv, "--service-id") != NULL ? json_option_value_runtime(argc, argv, "--service-id") : "0", NULL, 10);
13092 int code = emit_long_runtime(state_tenant_id_for_service_id_runtime(root, service_id));
13094 return code;
13095 }
13096 if (streq(name, "tenant-id-for-technician-id")) {
13097 long technician_id = strtol(json_option_value_runtime(argc, argv, "--technician-id") != NULL ? json_option_value_runtime(argc, argv, "--technician-id") : "0", NULL, 10);
13098 int code = emit_long_runtime(state_tenant_id_for_technician_id_runtime(root, technician_id));
13100 return code;
13101 }
13102 if (streq(name, "appointment-json-by-id")) {
13103 long appointment_id = strtol(json_option_value_runtime(argc, argv, "--appointment-id") != NULL ? json_option_value_runtime(argc, argv, "--appointment-id") : "0", NULL, 10);
13104 int code = emit_json_or_empty_runtime(state_find_appointment_by_id_runtime(root, appointment_id));
13106 return code;
13107 }
13108 if (streq(name, "service-duration-minutes")) {
13109 long service_id = strtol(json_option_value_runtime(argc, argv, "--service-id") != NULL ? json_option_value_runtime(argc, argv, "--service-id") : "0", NULL, 10);
13110 yyjson_val *service = state_find_service_by_id_runtime(root, service_id);
13111 int code = emit_long_runtime(service != NULL ? native_json_obj_get_long_default(service, "duration_minutes", 45, NULL) : 45);
13113 return code;
13114 }
13115 if (streq(name, "service-assigned-technician-ids-lines")) {
13116 long service_id = strtol(json_option_value_runtime(argc, argv, "--service-id") != NULL ? json_option_value_runtime(argc, argv, "--service-id") : "0", NULL, 10);
13117 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13118 int code = emit_service_assigned_technician_ids_lines_runtime(root, state_find_service_by_id_runtime(root, service_id), business_id);
13120 return code;
13121 }
13122 if (streq(name, "business-technician-ids-lines")) {
13123 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13124 int code = emit_business_technician_ids_lines_runtime(root, business_id);
13126 return code;
13127 }
13128 if (streq(name, "business-availability-lines")) {
13129 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13130 const char *day_name = json_option_value_runtime(argc, argv, "--day-of-week");
13131 int code = emit_business_availability_lines_runtime(root, business_id, day_name);
13133 return code;
13134 }
13135 if (streq(name, "technician-availability-lines")) {
13136 long technician_id = strtol(json_option_value_runtime(argc, argv, "--technician-id") != NULL ? json_option_value_runtime(argc, argv, "--technician-id") : "0", NULL, 10);
13137 const char *day_name = json_option_value_runtime(argc, argv, "--day-of-week");
13138 int code = emit_technician_availability_lines_runtime(root, technician_id, day_name);
13140 return code;
13141 }
13142 if (streq(name, "technician-appointments-lines")) {
13143 long technician_id = strtol(json_option_value_runtime(argc, argv, "--technician-id") != NULL ? json_option_value_runtime(argc, argv, "--technician-id") : "0", NULL, 10);
13144 const char *date_value = json_option_value_runtime(argc, argv, "--date");
13145 int code = emit_technician_appointments_lines_runtime(root, technician_id, date_value);
13147 return code;
13148 }
13149 if (streq(name, "business-open-for-slot")) {
13150 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13151 const char *date_value = json_option_value_runtime(argc, argv, "--date");
13152 const char *time_value = json_option_value_runtime(argc, argv, "--time");
13153 long duration_minutes = strtol(json_option_value_runtime(argc, argv, "--duration-minutes") != NULL ? json_option_value_runtime(argc, argv, "--duration-minutes") : "45", NULL, 10);
13154 int code = emit_text_line_runtime(business_open_for_slot_runtime(root, business_id, date_value, time_value, duration_minutes) ? "1" : "0");
13156 return code;
13157 }
13158 if (streq(name, "technician-booked-for-slot")) {
13159 long technician_id = strtol(json_option_value_runtime(argc, argv, "--technician-id") != NULL ? json_option_value_runtime(argc, argv, "--technician-id") : "0", NULL, 10);
13160 const char *date_value = json_option_value_runtime(argc, argv, "--date");
13161 const char *time_value = json_option_value_runtime(argc, argv, "--time");
13162 long duration_minutes = strtol(json_option_value_runtime(argc, argv, "--duration-minutes") != NULL ? json_option_value_runtime(argc, argv, "--duration-minutes") : "45", NULL, 10);
13163 int code = emit_text_line_runtime(technician_booked_for_slot_runtime(root, technician_id, date_value, time_value, duration_minutes) ? "1" : "0");
13165 return code;
13166 }
13167 if (streq(name, "assign-technician-for-slot")) {
13168 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13169 long service_id = strtol(json_option_value_runtime(argc, argv, "--service-id") != NULL ? json_option_value_runtime(argc, argv, "--service-id") : "0", NULL, 10);
13170 const char *date_value = json_option_value_runtime(argc, argv, "--date");
13171 const char *time_value = json_option_value_runtime(argc, argv, "--time");
13172 int code = emit_long_runtime(assign_technician_for_slot_runtime(root, business_id, service_id, date_value, time_value));
13174 return code;
13175 }
13176 if (streq(name, "active-tenant-ids-lines")) {
13177 int code = emit_active_tenant_ids_lines_runtime(root);
13179 return code;
13180 }
13181 if (streq(name, "user-membership-tenant-ids-json")) {
13182 long user_id = strtol(json_option_value_runtime(argc, argv, "--user-id") != NULL ? json_option_value_runtime(argc, argv, "--user-id") : "0", NULL, 10);
13183 int code = emit_user_membership_tenant_ids_json_runtime(root, user_id);
13185 return code;
13186 }
13187 if (streq(name, "manageable-business-cards-json")) {
13188 const char *tenant_ids_json = json_option_value_runtime(argc, argv, "--tenant-ids-json");
13189 yyjson_doc *tenant_ids_doc = NULL;
13190 yyjson_val *tenant_ids_root = load_root_from_text_runtime(tenant_ids_json != NULL ? tenant_ids_json : "[]", &tenant_ids_doc);
13191 int code = (tenant_ids_root != NULL && yyjson_is_arr(tenant_ids_root)) ? emit_manageable_business_cards_json_runtime(root, tenant_ids_root) : 64;
13192 native_json_doc_free(tenant_ids_doc);
13194 return code;
13195 }
13196 if (streq(name, "business-dashboard-page-json")) {
13197 const char *tenant_ids_json = json_option_value_runtime(argc, argv, "--tenant-ids-json");
13198 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
13199 yyjson_doc *tenant_ids_doc = NULL;
13200 yyjson_val *tenant_ids_root = load_root_from_text_runtime(tenant_ids_json != NULL ? tenant_ids_json : "[]", &tenant_ids_doc);
13201 int code = (tenant_ids_root != NULL && yyjson_is_arr(tenant_ids_root) && app_root != NULL && app_root[0] != '\0') ? emit_business_dashboard_page_json_runtime(root, tenant_ids_root, app_root) : 64;
13202 native_json_doc_free(tenant_ids_doc);
13204 return code;
13205 }
13206 if (streq(name, "explore-page-json")) {
13207 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
13208 int code = emit_explore_page_json_runtime(root, app_root);
13210 return code;
13211 }
13212 if (streq(name, "public-business-cards-json")) {
13215 return code;
13216 }
13217 if (streq(name, "managed-appointments-json")) {
13218 const char *tenant_ids_json = json_option_value_runtime(argc, argv, "--tenant-ids-json");
13219 yyjson_doc *tenant_ids_doc = NULL;
13220 yyjson_val *tenant_ids_root = load_root_from_text_runtime(tenant_ids_json != NULL ? tenant_ids_json : "[]", &tenant_ids_doc);
13221 int code = (tenant_ids_root != NULL && yyjson_is_arr(tenant_ids_root)) ? emit_managed_appointments_json_runtime(root, tenant_ids_root) : 64;
13222 native_json_doc_free(tenant_ids_doc);
13224 return code;
13225 }
13226 if (streq(name, "user-appointments-json")) {
13227 long user_id = strtol(json_option_value_runtime(argc, argv, "--user-id") != NULL ? json_option_value_runtime(argc, argv, "--user-id") : "0", NULL, 10);
13228 int code = emit_user_appointments_json_runtime(root, user_id);
13230 return code;
13231 }
13232 if (streq(name, "calendar-page-json")) {
13233 long user_id = strtol(json_option_value_runtime(argc, argv, "--user-id") != NULL ? json_option_value_runtime(argc, argv, "--user-id") : "0", NULL, 10);
13234 const char *tenant_ids_json = json_option_value_runtime(argc, argv, "--tenant-ids-json");
13235 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
13236 yyjson_doc *tenant_ids_doc = NULL;
13237 yyjson_val *tenant_ids_root = load_root_from_text_runtime(tenant_ids_json != NULL ? tenant_ids_json : "[]", &tenant_ids_doc);
13238 int code = (tenant_ids_root != NULL && yyjson_is_arr(tenant_ids_root) && app_root != NULL && app_root[0] != '\0') ? emit_calendar_page_json_runtime(root, user_id, tenant_ids_root, app_root) : 64;
13239 native_json_doc_free(tenant_ids_doc);
13241 return code;
13242 }
13243 if (streq(name, "profile-page-json")) {
13244 long user_id = strtol(json_option_value_runtime(argc, argv, "--user-id") != NULL ? json_option_value_runtime(argc, argv, "--user-id") : "0", NULL, 10);
13245 int code = emit_profile_page_json_runtime(root, user_id);
13247 return code;
13248 }
13249 if (streq(name, "appointments-page-json")) {
13250 long user_id = strtol(json_option_value_runtime(argc, argv, "--user-id") != NULL ? json_option_value_runtime(argc, argv, "--user-id") : "0", NULL, 10);
13251 const char *tenant_ids_json = json_option_value_runtime(argc, argv, "--tenant-ids-json");
13252 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
13253 yyjson_doc *tenant_ids_doc = NULL;
13254 yyjson_val *tenant_ids_root = load_root_from_text_runtime(tenant_ids_json != NULL ? tenant_ids_json : "[]", &tenant_ids_doc);
13255 int code = (tenant_ids_root != NULL && yyjson_is_arr(tenant_ids_root) && app_root != NULL && app_root[0] != '\0') ? emit_appointments_page_json_runtime(root, user_id, tenant_ids_root, app_root) : 64;
13256 native_json_doc_free(tenant_ids_doc);
13258 return code;
13259 }
13260 if (streq(name, "profile-render-page-json")) {
13261 long user_id = strtol(json_option_value_runtime(argc, argv, "--user-id") != NULL ? json_option_value_runtime(argc, argv, "--user-id") : "0", NULL, 10);
13262 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
13263 const char *payment_preference_options_html = json_option_value_runtime(argc, argv, "--payment-preference-options-html");
13264 int code = emit_profile_render_page_json_runtime(root, user_id, app_root, payment_preference_options_html);
13266 return code;
13267 }
13268 if (streq(name, "user-json-by-username")) {
13269 const char *username = json_option_value_runtime(argc, argv, "--username");
13272 return code;
13273 }
13274 if (streq(name, "user-json-by-email")) {
13275 const char *email = json_option_value_runtime(argc, argv, "--email");
13278 return code;
13279 }
13280 if (streq(name, "user-json-by-id")) {
13281 long user_id = strtol(json_option_value_runtime(argc, argv, "--user-id") != NULL ? json_option_value_runtime(argc, argv, "--user-id") : "0", NULL, 10);
13284 return code;
13285 }
13286 if (streq(name, "service-json-by-id")) {
13287 long service_id = strtol(json_option_value_runtime(argc, argv, "--service-id") != NULL ? json_option_value_runtime(argc, argv, "--service-id") : "0", NULL, 10);
13290 return code;
13291 }
13292 if (streq(name, "service-json-for-business")) {
13293 long service_id = strtol(json_option_value_runtime(argc, argv, "--service-id") != NULL ? json_option_value_runtime(argc, argv, "--service-id") : "0", NULL, 10);
13294 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13295 int code = emit_json_or_empty_runtime(state_find_service_for_business_runtime(root, service_id, business_id));
13297 return code;
13298 }
13299 if (streq(name, "business-availability-json-for-business")) {
13300 long availability_id = strtol(json_option_value_runtime(argc, argv, "--availability-id") != NULL ? json_option_value_runtime(argc, argv, "--availability-id") : "0", NULL, 10);
13301 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13302 int code = emit_json_or_empty_runtime(state_find_business_availability_for_business_runtime(root, availability_id, business_id));
13304 return code;
13305 }
13306 if (streq(name, "first-service-json-for-business")) {
13307 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13310 return code;
13311 }
13312 if (streq(name, "service-image-url-for-business")) {
13313 long service_id = strtol(json_option_value_runtime(argc, argv, "--service-id") != NULL ? json_option_value_runtime(argc, argv, "--service-id") : "0", NULL, 10);
13314 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13315 int code = emit_object_string_field_runtime(state_find_service_for_business_runtime(root, service_id, business_id), "image_url", "");
13317 return code;
13318 }
13319 if (streq(name, "service-sort-order-for-business")) {
13320 long service_id = strtol(json_option_value_runtime(argc, argv, "--service-id") != NULL ? json_option_value_runtime(argc, argv, "--service-id") : "0", NULL, 10);
13321 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13322 yyjson_val *service = state_find_service_for_business_runtime(root, service_id, business_id);
13323 long fallback = service != NULL ? native_json_obj_get_long_default(service, "id", 1, NULL) : 1;
13324 int code = emit_long_runtime(service != NULL ? native_json_obj_get_long_default(service, "sort_order", fallback, NULL) : 1);
13326 return code;
13327 }
13328 if (streq(name, "business-name-by-id")) {
13329 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13330 int code = emit_object_string_field_runtime(state_find_business_by_id_runtime(root, business_id), "name", "");
13332 return code;
13333 }
13334 if (streq(name, "business-slug-by-id")) {
13335 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13336 int code = emit_object_string_field_runtime(state_find_business_by_id_runtime(root, business_id), "slug", "");
13338 return code;
13339 }
13340 if (streq(name, "business-time-format-by-id")) {
13341 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13342 int code = emit_object_string_field_runtime(state_find_business_by_id_runtime(root, business_id), "time_format", "am_pm");
13344 return code;
13345 }
13346 if (streq(name, "business-admin-service-rows-json")) {
13347 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13348 int code = emit_business_admin_service_rows_json_runtime(root, business_id);
13350 return code;
13351 }
13352 if (streq(name, "business-admin-technician-rows-json")) {
13353 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13354 int code = emit_business_admin_technician_rows_json_runtime(root, business_id);
13356 return code;
13357 }
13358 if (streq(name, "business-public-page-json")) {
13359 const char *slug = json_option_value_runtime(argc, argv, "--slug");
13360 long user_id = strtol(json_option_value_runtime(argc, argv, "--user-id") != NULL ? json_option_value_runtime(argc, argv, "--user-id") : "0", NULL, 10);
13361 int code = emit_business_public_page_json_runtime(root, slug, user_id);
13363 return code;
13364 }
13365 if (streq(name, "business-edit-page-json")) {
13366 const char *slug = json_option_value_runtime(argc, argv, "--slug");
13367 int code = emit_business_edit_page_json_runtime(root, slug);
13369 return code;
13370 }
13371 if (streq(name, "service-edit-page-json")) {
13372 const char *slug = json_option_value_runtime(argc, argv, "--slug");
13373 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
13374 long service_id = strtol(json_option_value_runtime(argc, argv, "--service-id") != NULL ? json_option_value_runtime(argc, argv, "--service-id") : "0", NULL, 10);
13375 int code = emit_service_edit_page_json_runtime(root, slug, service_id, app_root);
13377 return code;
13378 }
13379 if (streq(name, "technician-edit-page-json")) {
13380 const char *slug = json_option_value_runtime(argc, argv, "--slug");
13381 long technician_id = strtol(json_option_value_runtime(argc, argv, "--technician-id") != NULL ? json_option_value_runtime(argc, argv, "--technician-id") : "0", NULL, 10);
13382 int code = emit_technician_edit_page_json_runtime(root, slug, technician_id);
13384 return code;
13385 }
13386 if (streq(name, "business-public-render-page-json")) {
13387 const char *slug = json_option_value_runtime(argc, argv, "--slug");
13388 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
13389 long user_id = strtol(json_option_value_runtime(argc, argv, "--user-id") != NULL ? json_option_value_runtime(argc, argv, "--user-id") : "0", NULL, 10);
13390 int code = emit_business_public_render_page_json_runtime(root, slug, user_id, app_root);
13392 return code;
13393 }
13394 if (streq(name, "business-calendar-page-json")) {
13395 const char *slug = json_option_value_runtime(argc, argv, "--slug");
13396 int code = emit_business_calendar_page_json_runtime(root, slug);
13398 return code;
13399 }
13400 if (streq(name, "business-calendar-render-page-json")) {
13401 const char *slug = json_option_value_runtime(argc, argv, "--slug");
13402 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
13403 const char *selected_year = json_option_value_runtime(argc, argv, "--selected-year");
13404 const char *selected_month = json_option_value_runtime(argc, argv, "--selected-month");
13405 const char *selected_date = json_option_value_runtime(argc, argv, "--selected-date");
13406 int code = emit_business_calendar_render_page_json_runtime(root, slug, app_root, selected_year, selected_month, selected_date);
13408 return code;
13409 }
13410 if (streq(name, "service-book-form-page-json")) {
13411 const char *slug = json_option_value_runtime(argc, argv, "--slug");
13412 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
13413 const char *error_message = json_option_value_runtime(argc, argv, "--error-message");
13414 long service_id = strtol(json_option_value_runtime(argc, argv, "--service-id") != NULL ? json_option_value_runtime(argc, argv, "--service-id") : "0", NULL, 10);
13415 int code = emit_service_book_form_page_json_runtime(root, slug, service_id, app_root, error_message);
13417 return code;
13418 }
13419 if (streq(name, "service-visual-html")) {
13420 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
13421 const char *image_url = json_option_value_runtime(argc, argv, "--image-url");
13422 const char *service_name = json_option_value_runtime(argc, argv, "--service-name");
13423 char *html = render_service_visual_html_dup_runtime(app_root, image_url != NULL ? image_url : "", service_name != NULL ? service_name : "");
13424 int code = html != NULL ? 0 : 69;
13425 if (html != NULL) {
13426 fputs(html, stdout);
13427 }
13428 free(html);
13430 return code;
13431 }
13432 if (streq(name, "service-sort-order-options-html")) {
13433 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
13434 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13435 long selected_order = strtol(json_option_value_runtime(argc, argv, "--selected-order") != NULL ? json_option_value_runtime(argc, argv, "--selected-order") : "0", NULL, 10);
13436 int include_append = strtol(json_option_value_runtime(argc, argv, "--include-append") != NULL ? json_option_value_runtime(argc, argv, "--include-append") : "0", NULL, 10) != 0;
13437 char *html = render_service_sort_order_options_html_dup_runtime(root, business_id, selected_order, include_append, app_root);
13438 int code = html != NULL ? 0 : 69;
13439 if (html != NULL) {
13440 fputs(html, stdout);
13441 }
13442 free(html);
13444 return code;
13445 }
13446 if (streq(name, "service-booking-options-editor-html")) {
13447 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
13448 long service_id = strtol(json_option_value_runtime(argc, argv, "--service-id") != NULL ? json_option_value_runtime(argc, argv, "--service-id") : "0", NULL, 10);
13449 char *html = render_service_booking_options_editor_html_dup_runtime(root, service_id, app_root);
13450 int code = html != NULL ? 0 : 69;
13451 if (html != NULL) {
13452 fputs(html, stdout);
13453 }
13454 free(html);
13456 return code;
13457 }
13458 if (streq(name, "service-technician-checkboxes-html")) {
13459 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
13460 const char *empty_message = json_option_value_runtime(argc, argv, "--empty-message");
13461 long service_id = strtol(json_option_value_runtime(argc, argv, "--service-id") != NULL ? json_option_value_runtime(argc, argv, "--service-id") : "0", NULL, 10);
13462 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13463 char *html = render_technician_checkboxes_html_dup_runtime(root, business_id, service_id, empty_message != NULL ? empty_message : "", 0, app_root);
13464 int code = html != NULL ? 0 : 69;
13465 if (html != NULL) {
13466 fputs(html, stdout);
13467 }
13468 free(html);
13470 return code;
13471 }
13472 if (streq(name, "business-technician-checkboxes-html")) {
13473 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
13474 const char *empty_message = json_option_value_runtime(argc, argv, "--empty-message");
13475 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13476 char *html = render_technician_checkboxes_html_dup_runtime(root, business_id, 0, empty_message != NULL ? empty_message : "", 1, app_root);
13477 int code = html != NULL ? 0 : 69;
13478 if (html != NULL) {
13479 fputs(html, stdout);
13480 }
13481 free(html);
13483 return code;
13484 }
13485 if (streq(name, "business-admin-page-json")) {
13486 const char *slug = json_option_value_runtime(argc, argv, "--slug");
13487 const char *app_root = json_option_value_runtime(argc, argv, "--app-root");
13488 const char *invite_account_type_options_html = json_option_value_runtime(argc, argv, "--invite-account-type-options-html");
13489 int code = emit_business_admin_page_json_runtime(root, slug, app_root, invite_account_type_options_html);
13491 return code;
13492 }
13493 if (streq(name, "business-image-url-by-slug")) {
13494 const char *slug = json_option_value_runtime(argc, argv, "--slug");
13495 int code = emit_object_string_field_runtime(state_find_business_by_slug_runtime(root, slug), "image_url", "");
13497 return code;
13498 }
13499 if (streq(name, "tenant-id-for-business-id")) {
13500 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13501 int code = emit_long_runtime(state_tenant_id_for_business_id_runtime(root, business_id));
13503 return code;
13504 }
13505 if (streq(name, "google-calendar-connection-for-business")) {
13506 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13509 return code;
13510 }
13511 if (streq(name, "booking-json-for-user")) {
13512 long booking_id = strtol(json_option_value_runtime(argc, argv, "--booking-id") != NULL ? json_option_value_runtime(argc, argv, "--booking-id") : "0", NULL, 10);
13513 long user_id = strtol(json_option_value_runtime(argc, argv, "--user-id") != NULL ? json_option_value_runtime(argc, argv, "--user-id") : "0", NULL, 10);
13514 int code = emit_json_or_empty_runtime(state_find_booking_for_user_runtime(root, booking_id, user_id));
13516 return code;
13517 }
13518 if (streq(name, "technician-json-for-business")) {
13519 long technician_id = strtol(json_option_value_runtime(argc, argv, "--technician-id") != NULL ? json_option_value_runtime(argc, argv, "--technician-id") : "0", NULL, 10);
13520 long business_id = strtol(json_option_value_runtime(argc, argv, "--business-id") != NULL ? json_option_value_runtime(argc, argv, "--business-id") : "0", NULL, 10);
13521 int code = emit_json_or_empty_runtime(state_find_technician_for_business_runtime(root, technician_id, business_id));
13523 return code;
13524 }
13525 if (streq(name, "service-booking-options-array-json")) {
13526 long service_id = strtol(json_option_value_runtime(argc, argv, "--service-id") != NULL ? json_option_value_runtime(argc, argv, "--service-id") : "0", NULL, 10);
13527 yyjson_val *service = state_find_service_by_id_runtime(root, service_id);
13528 yyjson_val *options = service != NULL ? native_json_obj_get_array(service, "booking_options") : NULL;
13529 if (options == NULL) {
13530 fputs("[]", stdout);
13532 return 0;
13533 }
13534 {
13535 int code = emit_compact_json_value_runtime(options);
13537 return code;
13538 }
13539 }
13540 if (streq(name, "service-booking-options-json")) {
13541 long service_id = strtol(json_option_value_runtime(argc, argv, "--service-id") != NULL ? json_option_value_runtime(argc, argv, "--service-id") : "0", NULL, 10);
13542 yyjson_val *service = state_find_service_by_id_runtime(root, service_id);
13543 if (service == NULL) {
13544 fputs("[]", stdout);
13546 return 0;
13547 }
13548 {
13549 char *name_copy = native_json_obj_get_string_dup(service, "name");
13550 char *cost_copy = native_json_obj_get_string_dup(service, "cost");
13551 long duration = native_json_obj_get_long_default(service, "duration_minutes", 45, NULL);
13552 yyjson_val *booking_options = native_json_obj_get_array(service, "booking_options");
13553 fprintf(stdout, "[{\"label\":");
13554 if (name_copy != NULL) {
13555 yyjson_mut_doc *tmp_doc = yyjson_mut_doc_new(NULL);
13556 yyjson_mut_val *tmp_val = yyjson_mut_strcpy(tmp_doc, name_copy);
13557 char *name_json = yyjson_mut_val_write(tmp_val, YYJSON_WRITE_NOFLAG, NULL);
13558 fprintf(stdout, "%s", name_json != NULL ? name_json : "\"\"");
13559 free(name_json);
13560 yyjson_mut_doc_free(tmp_doc);
13561 } else {
13562 fputs("\"\"", stdout);
13563 }
13564 fprintf(stdout, ",\"duration_minutes\":%ld,\"cost\":", duration);
13565 if (cost_copy != NULL) {
13566 yyjson_mut_doc *tmp_doc = yyjson_mut_doc_new(NULL);
13567 yyjson_mut_val *tmp_val = yyjson_mut_strcpy(tmp_doc, cost_copy);
13568 char *cost_json = yyjson_mut_val_write(tmp_val, YYJSON_WRITE_NOFLAG, NULL);
13569 fprintf(stdout, "%s", cost_json != NULL ? cost_json : "\"\"");
13570 free(cost_json);
13571 yyjson_mut_doc_free(tmp_doc);
13572 } else {
13573 fputs("\"\"", stdout);
13574 }
13575 fputs("}", stdout);
13576 if (booking_options != NULL) {
13577 yyjson_arr_iter iter;
13578 yyjson_val *item = NULL;
13579 if (yyjson_arr_iter_init(booking_options, &iter)) {
13580 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
13581 char *text = native_json_serialize_compact_dup(item);
13582 if (text != NULL) {
13583 fputc(',', stdout);
13584 fputs(text, stdout);
13585 free(text);
13586 }
13587 }
13588 }
13589 }
13590 fputs("]", stdout);
13591 free(name_copy);
13592 free(cost_copy);
13594 return 0;
13595 }
13596 }
13597 if (streq(name, "service-selected-booking-option-json")) {
13598 long service_id = strtol(json_option_value_runtime(argc, argv, "--service-id") != NULL ? json_option_value_runtime(argc, argv, "--service-id") : "0", NULL, 10);
13599 long booking_option_index = strtol(json_option_value_runtime(argc, argv, "--booking-option-index") != NULL ? json_option_value_runtime(argc, argv, "--booking-option-index") : "0", NULL, 10);
13600 yyjson_val *service = state_find_service_by_id_runtime(root, service_id);
13601 yyjson_val *booking_options = service != NULL ? native_json_obj_get_array(service, "booking_options") : NULL;
13602 if (service == NULL) {
13603 fputs("{}", stdout);
13605 return 0;
13606 }
13607 if (booking_option_index <= 0) {
13608 booking_option_index = 0;
13609 }
13610 if (booking_options != NULL && yyjson_arr_size(booking_options) > 0) {
13611 yyjson_val *item = yyjson_arr_get(booking_options, (size_t)booking_option_index);
13612 if (item == NULL) {
13613 item = yyjson_arr_get(booking_options, 0);
13614 }
13615 if (item != NULL) {
13616 int code = emit_compact_json_value_runtime(item);
13618 return code;
13619 }
13620 }
13621 fputs("{}", stdout);
13623 return 0;
13624 }
13625 if (streq(name, "event-stream-row-json-lines")) {
13626 const char *since_id_text = json_option_value_runtime(argc, argv, "--since-id");
13627 const char *limit_text = json_option_value_runtime(argc, argv, "--limit");
13628 const char *subscription_scope_json = json_option_value_runtime(argc, argv, "--subscription-scope-json");
13629 const char *filters_json = json_option_value_runtime(argc, argv, "--filters-json");
13630 yyjson_doc *subscription_scope_doc = NULL;
13631 yyjson_doc *filters_doc = NULL;
13632 yyjson_val *subscription_scope_root = load_root_from_text_runtime(subscription_scope_json != NULL ? subscription_scope_json : "{\"scope_kind\":\"all\"}", &subscription_scope_doc);
13633 yyjson_val *filters_root = load_root_from_text_runtime(filters_json != NULL ? filters_json : "{}", &filters_doc);
13634 long since_id = strtol(since_id_text != NULL ? since_id_text : "0", NULL, 10);
13635 long limit_value = strtol(limit_text != NULL ? limit_text : "25", NULL, 10);
13636 int code = 0;
13637 if (limit_value <= 0) {
13638 limit_value = 25;
13639 }
13640 if (limit_value > 100) {
13641 limit_value = 100;
13642 }
13643 code = handle_state_event_stream_row_json_lines_command(root, subscription_scope_root, filters_root, since_id, limit_value);
13644 native_json_doc_free(subscription_scope_doc);
13645 native_json_doc_free(filters_doc);
13647 return code;
13648 }
13649 if (streq(name, "next-id")) {
13650 const char *section_name = json_option_value_runtime(argc, argv, "--section");
13651 int code = 0;
13652 if (section_name == NULL || section_name[0] == '\0') {
13654 return 64;
13655 }
13656 code = emit_long_runtime(max_id_in_state_section_runtime(root, section_name) + 1);
13658 return code;
13659 }
13660 if (streq(name, "append-record")) {
13661 const char *section_name = json_option_value_runtime(argc, argv, "--section");
13662 const char *record_json = json_option_value_runtime(argc, argv, "--record-json");
13663 yyjson_doc *record_doc = NULL;
13664 yyjson_val *record_root = NULL;
13665 yyjson_mut_doc *mut_doc = NULL;
13666 yyjson_mut_val *mut_root = NULL;
13667 yyjson_mut_val *section = NULL;
13668 int code = 69;
13669 if (section_name == NULL || section_name[0] == '\0' || record_json == NULL || record_json[0] == '\0') {
13671 return 64;
13672 }
13673 record_root = load_root_from_text_runtime(record_json, &record_doc);
13674 if (record_root == NULL || !yyjson_is_obj(record_root)) {
13676 native_json_doc_free(record_doc);
13677 return 64;
13678 }
13679 mut_doc = yyjson_doc_mut_copy(doc, NULL);
13680 mut_root = yyjson_mut_doc_get_root(mut_doc);
13681 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
13683 native_json_doc_free(record_doc);
13684 yyjson_mut_doc_free(mut_doc);
13685 return 69;
13686 }
13687 section = ensure_mut_array_field_runtime(mut_doc, mut_root, section_name);
13688 if (section == NULL || !yyjson_mut_arr_append(section, yyjson_val_mut_copy(mut_doc, record_root))) {
13690 native_json_doc_free(record_doc);
13691 yyjson_mut_doc_free(mut_doc);
13692 return 69;
13693 }
13694 code = write_mutable_json_doc_runtime(mut_doc, state_path);
13696 native_json_doc_free(record_doc);
13697 yyjson_mut_doc_free(mut_doc);
13698 return code;
13699 }
13700 if (streq(name, "update-record-by-id")) {
13701 const char *section_name = json_option_value_runtime(argc, argv, "--section");
13702 const char *record_id_text = json_option_value_runtime(argc, argv, "--record-id");
13703 const char *patch_json = json_option_value_runtime(argc, argv, "--patch-json");
13704 yyjson_doc *patch_doc = NULL;
13705 yyjson_val *patch_root = NULL;
13706 yyjson_mut_doc *mut_doc = NULL;
13707 yyjson_mut_val *mut_root = NULL;
13708 yyjson_mut_val *section = NULL;
13709 yyjson_mut_val *record = NULL;
13710 long record_id = strtol(record_id_text != NULL ? record_id_text : "0", NULL, 10);
13711 int code = 69;
13712 if (section_name == NULL || section_name[0] == '\0' || record_id <= 0 || patch_json == NULL || patch_json[0] == '\0') {
13714 return 64;
13715 }
13716 patch_root = load_root_from_text_runtime(patch_json, &patch_doc);
13717 if (patch_root == NULL || !yyjson_is_obj(patch_root)) {
13719 native_json_doc_free(patch_doc);
13720 return 64;
13721 }
13722 mut_doc = yyjson_doc_mut_copy(doc, NULL);
13723 mut_root = yyjson_mut_doc_get_root(mut_doc);
13724 section = mut_root != NULL ? yyjson_mut_obj_get(mut_root, section_name) : NULL;
13725 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root) || section == NULL || !yyjson_mut_is_arr(section)) {
13727 native_json_doc_free(patch_doc);
13728 yyjson_mut_doc_free(mut_doc);
13729 return 66;
13730 }
13731 record = find_mut_array_item_by_long_field_runtime(section, "id", record_id);
13732 if (record == NULL || !merge_patch_into_mut_object_runtime(mut_doc, record, patch_root)) {
13734 native_json_doc_free(patch_doc);
13735 yyjson_mut_doc_free(mut_doc);
13736 return record == NULL ? 66 : 69;
13737 }
13738 code = write_mutable_json_doc_runtime(mut_doc, state_path);
13740 native_json_doc_free(patch_doc);
13741 yyjson_mut_doc_free(mut_doc);
13742 return code;
13743 }
13744 if (streq(name, "remove-record-by-id")) {
13745 const char *section_name = json_option_value_runtime(argc, argv, "--section");
13746 const char *record_id_text = json_option_value_runtime(argc, argv, "--record-id");
13747 yyjson_mut_doc *mut_doc = NULL;
13748 yyjson_mut_val *mut_root = NULL;
13749 yyjson_mut_val *section = NULL;
13750 size_t idx = 0;
13751 size_t max = 0;
13752 yyjson_mut_val *item = NULL;
13753 long record_id = strtol(record_id_text != NULL ? record_id_text : "0", NULL, 10);
13754 int code = 66;
13755 if (section_name == NULL || section_name[0] == '\0' || record_id <= 0) {
13757 return 64;
13758 }
13759 mut_doc = yyjson_doc_mut_copy(doc, NULL);
13760 mut_root = yyjson_mut_doc_get_root(mut_doc);
13761 section = mut_root != NULL ? yyjson_mut_obj_get(mut_root, section_name) : NULL;
13762 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root) || section == NULL || !yyjson_mut_is_arr(section)) {
13764 yyjson_mut_doc_free(mut_doc);
13765 return 66;
13766 }
13767 yyjson_mut_arr_foreach(section, idx, max, item) {
13768 if (yyjson_mut_is_obj(item) && value_long_equals_runtime((yyjson_val *)yyjson_mut_obj_get(item, "id"), record_id)) {
13769 yyjson_mut_arr_remove(section, idx);
13770 code = write_mutable_json_doc_runtime(mut_doc, state_path);
13772 yyjson_mut_doc_free(mut_doc);
13773 return code;
13774 }
13775 }
13777 yyjson_mut_doc_free(mut_doc);
13778 return code;
13779 }
13780 if (streq(name, "prune-rate-limit-events")) {
13781 const char *cutoff_iso = json_option_value_runtime(argc, argv, "--cutoff-iso");
13782 yyjson_mut_doc *mut_doc = NULL;
13783 yyjson_mut_val *mut_root = NULL;
13784 yyjson_mut_val *events = NULL;
13785 size_t idx = 0;
13786 int code = 69;
13787 if (cutoff_iso == NULL || cutoff_iso[0] == '\0') {
13789 return 64;
13790 }
13791 mut_doc = yyjson_doc_mut_copy(doc, NULL);
13792 mut_root = yyjson_mut_doc_get_root(mut_doc);
13793 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
13795 yyjson_mut_doc_free(mut_doc);
13796 return 69;
13797 }
13798 events = ensure_mut_array_field_runtime(mut_doc, mut_root, "rate_limit_events");
13799 if (events == NULL) {
13801 yyjson_mut_doc_free(mut_doc);
13802 return 69;
13803 }
13804 for (idx = yyjson_mut_arr_size(events); idx > 0; idx--) {
13805 yyjson_mut_val *item = yyjson_mut_arr_get(events, idx - 1);
13806 char *created_at = NULL;
13807 if (!yyjson_mut_is_obj(item)) {
13808 continue;
13809 }
13810 created_at = native_json_obj_get_string_dup((yyjson_val *)item, "created_at");
13811 if (created_at == NULL || created_at[0] == '\0' || strcmp(created_at, cutoff_iso) < 0) {
13812 yyjson_mut_arr_remove(events, idx - 1);
13813 }
13814 free(created_at);
13815 }
13816 code = write_mutable_json_doc_runtime(mut_doc, state_path);
13818 yyjson_mut_doc_free(mut_doc);
13819 return code;
13820 }
13821 if (streq(name, "rate-limit-event-count")) {
13822 const char *policy_key = json_option_value_runtime(argc, argv, "--policy-key");
13823 const char *bucket_key = json_option_value_runtime(argc, argv, "--bucket-key");
13824 const char *ip_address = json_option_value_runtime(argc, argv, "--ip-address");
13825 const char *window_start = json_option_value_runtime(argc, argv, "--window-start");
13826 yyjson_arr_iter iter;
13827 yyjson_val *events = state_array_runtime(root, "rate_limit_events");
13828 yyjson_val *item = NULL;
13829 long count = 0;
13830 if (policy_key == NULL || bucket_key == NULL || window_start == NULL) {
13832 return 64;
13833 }
13834 if (events != NULL && yyjson_arr_iter_init(events, &iter)) {
13835 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
13836 char *created_at = NULL;
13837 char *event_ip = NULL;
13838 int ip_match = 0;
13839 if (!yyjson_is_obj(item)) {
13840 continue;
13841 }
13842 if (!yyjson_equals_str(native_json_obj_get(item, "policy_key"), policy_key) || !yyjson_equals_str(native_json_obj_get(item, "bucket_key"), bucket_key)) {
13843 continue;
13844 }
13845 created_at = native_json_obj_get_string_dup(item, "created_at");
13846 event_ip = native_json_obj_get_string_dup(item, "ip_address");
13847 ip_match = ((ip_address == NULL || ip_address[0] == '\0') && (event_ip == NULL || event_ip[0] == '\0')) || ((ip_address != NULL && ip_address[0] != '\0') && event_ip != NULL && streq(event_ip, ip_address));
13848 if (ip_match && created_at != NULL && strcmp(created_at, window_start) >= 0) {
13849 count++;
13850 }
13851 free(created_at);
13852 free(event_ip);
13853 }
13854 }
13856 return emit_long_runtime(count);
13857 }
13858 if (streq(name, "record-rate-limit-event")) {
13859 const char *policy_key = json_option_value_runtime(argc, argv, "--policy-key");
13860 const char *bucket_key = json_option_value_runtime(argc, argv, "--bucket-key");
13861 const char *ip_address = json_option_value_runtime(argc, argv, "--ip-address");
13862 const char *outcome = json_option_value_runtime(argc, argv, "--outcome");
13863 const char *created_at = json_option_value_runtime(argc, argv, "--created-at");
13864 yyjson_mut_doc *mut_doc = NULL;
13865 yyjson_mut_val *mut_root = NULL;
13866 yyjson_mut_val *events = NULL;
13867 yyjson_mut_val *event_obj = NULL;
13868 long event_id = max_id_in_state_section_runtime(root, "rate_limit_events") + 1;
13869 int code = 69;
13870 if (policy_key == NULL || policy_key[0] == '\0' || bucket_key == NULL || bucket_key[0] == '\0' || outcome == NULL || outcome[0] == '\0' || created_at == NULL || created_at[0] == '\0') {
13872 return 64;
13873 }
13874 mut_doc = yyjson_doc_mut_copy(doc, NULL);
13875 mut_root = yyjson_mut_doc_get_root(mut_doc);
13876 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
13878 yyjson_mut_doc_free(mut_doc);
13879 return 69;
13880 }
13881 events = ensure_mut_array_field_runtime(mut_doc, mut_root, "rate_limit_events");
13882 event_obj = yyjson_mut_obj(mut_doc);
13883 if (events == NULL || event_obj == NULL || !yyjson_mut_obj_add_int(mut_doc, event_obj, "id", event_id) || !yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "policy_key", policy_key) || !yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "bucket_key", bucket_key) || !yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "outcome", outcome) || !yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "created_at", created_at)) {
13885 yyjson_mut_doc_free(mut_doc);
13886 return 69;
13887 }
13888 if (ip_address != NULL && ip_address[0] != '\0') {
13889 if (!yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "ip_address", ip_address)) {
13891 yyjson_mut_doc_free(mut_doc);
13892 return 69;
13893 }
13894 } else if (!yyjson_mut_obj_put(event_obj, yyjson_mut_strcpy(mut_doc, "ip_address"), yyjson_mut_null(mut_doc))) {
13896 yyjson_mut_doc_free(mut_doc);
13897 return 69;
13898 }
13899 if (!yyjson_mut_arr_append(events, event_obj)) {
13901 yyjson_mut_doc_free(mut_doc);
13902 return 69;
13903 }
13904 code = write_mutable_json_doc_runtime(mut_doc, state_path);
13906 yyjson_mut_doc_free(mut_doc);
13907 return code;
13908 }
13909 if (streq(name, "append-audit-event")) {
13910 const char *event_kind = json_option_value_runtime(argc, argv, "--event-kind");
13911 const char *created_at = json_option_value_runtime(argc, argv, "--created-at");
13912 const char *payload_json = json_option_value_runtime(argc, argv, "--payload-json");
13913 yyjson_mut_doc *mut_doc = NULL;
13914 yyjson_mut_val *mut_root = NULL;
13915 yyjson_mut_val *audit_log = NULL;
13916 yyjson_mut_val *event_obj = NULL;
13917 int code = 69;
13918 if (event_kind == NULL || event_kind[0] == '\0' || created_at == NULL || created_at[0] == '\0') {
13920 return 64;
13921 }
13922 mut_doc = yyjson_doc_mut_copy(doc, NULL);
13923 mut_root = yyjson_mut_doc_get_root(mut_doc);
13924 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
13926 yyjson_mut_doc_free(mut_doc);
13927 return 69;
13928 }
13929 audit_log = ensure_mut_array_field_runtime(mut_doc, mut_root, "audit_log");
13930 event_obj = normalized_payload_object_mut_runtime(mut_doc, payload_json != NULL ? payload_json : "{}");
13931 if (audit_log == NULL || event_obj == NULL || !yyjson_mut_is_obj(event_obj)) {
13933 yyjson_mut_doc_free(mut_doc);
13934 return 69;
13935 }
13936 yyjson_mut_obj_remove_key(event_obj, "event");
13937 yyjson_mut_obj_remove_key(event_obj, "created_at");
13938 if (!yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "event", event_kind) || !yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "created_at", created_at) || !yyjson_mut_arr_append(audit_log, event_obj)) {
13940 yyjson_mut_doc_free(mut_doc);
13941 return 69;
13942 }
13943 code = write_mutable_json_doc_runtime(mut_doc, state_path);
13945 yyjson_mut_doc_free(mut_doc);
13946 return code;
13947 }
13948 if (streq(name, "append-workflow-event")) {
13949 const char *event_kind = json_option_value_runtime(argc, argv, "--event-kind");
13950 const char *created_at = json_option_value_runtime(argc, argv, "--created-at");
13951 const char *payload_json = json_option_value_runtime(argc, argv, "--payload-json");
13952 yyjson_mut_doc *mut_doc = NULL;
13953 yyjson_mut_val *mut_root = NULL;
13954 yyjson_mut_val *events = NULL;
13955 yyjson_mut_val *event_obj = NULL;
13956 long event_id = 0;
13957 int code = 69;
13958 if (event_kind == NULL || event_kind[0] == '\0' || created_at == NULL || created_at[0] == '\0') {
13960 return 64;
13961 }
13962 event_id = max_id_in_state_section_runtime(root, "workflow_execution_events") + 1;
13963 mut_doc = yyjson_doc_mut_copy(doc, NULL);
13964 mut_root = yyjson_mut_doc_get_root(mut_doc);
13965 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
13967 yyjson_mut_doc_free(mut_doc);
13968 return 69;
13969 }
13970 events = ensure_mut_array_field_runtime(mut_doc, mut_root, "workflow_execution_events");
13971 event_obj = yyjson_mut_obj(mut_doc);
13972 if (events == NULL || event_obj == NULL || !yyjson_mut_obj_add_int(mut_doc, event_obj, "id", event_id) || !yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "event_kind", event_kind) || !yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "created_at", created_at)) {
13974 yyjson_mut_doc_free(mut_doc);
13975 return 69;
13976 }
13977 {
13978 yyjson_mut_val *payload_obj = normalized_payload_object_mut_runtime(mut_doc, payload_json != NULL ? payload_json : "{}");
13979 if (payload_obj == NULL || !yyjson_mut_obj_add(event_obj, yyjson_mut_strcpy(mut_doc, "payload_json"), payload_obj) || !yyjson_mut_arr_append(events, event_obj)) {
13981 yyjson_mut_doc_free(mut_doc);
13982 return 69;
13983 }
13984 }
13985 code = write_mutable_json_doc_runtime(mut_doc, state_path);
13987 yyjson_mut_doc_free(mut_doc);
13988 return code;
13989 }
13990 if (streq(name, "issue-browser-session")) {
13991 const char *user_id_text = json_option_value_runtime(argc, argv, "--user-id");
13992 const char *session_token_hash = json_option_value_runtime(argc, argv, "--session-token-hash");
13993 const char *created_at = json_option_value_runtime(argc, argv, "--created-at");
13994 const char *expires_at = json_option_value_runtime(argc, argv, "--expires-at");
13995 const char *user_agent = json_option_value_runtime(argc, argv, "--user-agent");
13996 const char *ip_address = json_option_value_runtime(argc, argv, "--ip-address");
13997 yyjson_mut_doc *mut_doc = NULL;
13998 yyjson_mut_val *mut_root = NULL;
13999 yyjson_mut_val *sessions = NULL;
14000 yyjson_mut_val *session_obj = NULL;
14001 long user_id = strtol(user_id_text != NULL ? user_id_text : "0", NULL, 10);
14002 long session_id = max_id_in_state_section_runtime(root, "sessions") + 1;
14003 int code = 69;
14004 if (user_id <= 0 || session_token_hash == NULL || session_token_hash[0] == '\0' || created_at == NULL || created_at[0] == '\0' || expires_at == NULL || expires_at[0] == '\0') {
14006 return 64;
14007 }
14008 mut_doc = yyjson_doc_mut_copy(doc, NULL);
14009 mut_root = yyjson_mut_doc_get_root(mut_doc);
14010 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
14012 yyjson_mut_doc_free(mut_doc);
14013 return 69;
14014 }
14015 sessions = ensure_mut_array_field_runtime(mut_doc, mut_root, "sessions");
14016 session_obj = yyjson_mut_obj(mut_doc);
14017 if (sessions == NULL || session_obj == NULL || !yyjson_mut_obj_add_int(mut_doc, session_obj, "id", session_id) || !yyjson_mut_obj_add_int(mut_doc, session_obj, "user_id", user_id) || !yyjson_mut_obj_add_strcpy(mut_doc, session_obj, "session_token_hash", session_token_hash) || !yyjson_mut_obj_add_strcpy(mut_doc, session_obj, "session_kind", "browser_session") || !yyjson_mut_obj_put(session_obj, yyjson_mut_strcpy(mut_doc, "current_tenant_id"), yyjson_mut_null(mut_doc)) || !yyjson_mut_obj_add_strcpy(mut_doc, session_obj, "expires_at", expires_at) || !yyjson_mut_obj_put(session_obj, yyjson_mut_strcpy(mut_doc, "revoked_at"), yyjson_mut_null(mut_doc)) || !yyjson_mut_obj_add_strcpy(mut_doc, session_obj, "created_at", created_at)) {
14019 yyjson_mut_doc_free(mut_doc);
14020 return 69;
14021 }
14022 if (user_agent != NULL && user_agent[0] != '\0') {
14023 if (!yyjson_mut_obj_add_strcpy(mut_doc, session_obj, "user_agent", user_agent)) {
14025 yyjson_mut_doc_free(mut_doc);
14026 return 69;
14027 }
14028 } else if (!yyjson_mut_obj_put(session_obj, yyjson_mut_strcpy(mut_doc, "user_agent"), yyjson_mut_null(mut_doc))) {
14030 yyjson_mut_doc_free(mut_doc);
14031 return 69;
14032 }
14033 if (ip_address != NULL && ip_address[0] != '\0') {
14034 if (!yyjson_mut_obj_add_strcpy(mut_doc, session_obj, "ip_address", ip_address)) {
14036 yyjson_mut_doc_free(mut_doc);
14037 return 69;
14038 }
14039 } else if (!yyjson_mut_obj_put(session_obj, yyjson_mut_strcpy(mut_doc, "ip_address"), yyjson_mut_null(mut_doc))) {
14041 yyjson_mut_doc_free(mut_doc);
14042 return 69;
14043 }
14044 if (!yyjson_mut_arr_append(sessions, session_obj)) {
14046 yyjson_mut_doc_free(mut_doc);
14047 return 69;
14048 }
14049 code = write_mutable_json_doc_runtime(mut_doc, state_path);
14051 yyjson_mut_doc_free(mut_doc);
14052 return code;
14053 }
14054 if (streq(name, "revoke-session-hash")) {
14055 const char *session_token_hash = json_option_value_runtime(argc, argv, "--session-token-hash");
14056 const char *revoked_at = json_option_value_runtime(argc, argv, "--revoked-at");
14057 yyjson_mut_doc *mut_doc = NULL;
14058 yyjson_mut_val *mut_root = NULL;
14059 yyjson_mut_val *sessions = NULL;
14060 yyjson_mut_val *session = NULL;
14061 size_t idx = 0;
14062 size_t max = 0;
14063 int code = 69;
14064 if (session_token_hash == NULL || session_token_hash[0] == '\0' || revoked_at == NULL || revoked_at[0] == '\0') {
14066 return 64;
14067 }
14068 mut_doc = yyjson_doc_mut_copy(doc, NULL);
14069 mut_root = yyjson_mut_doc_get_root(mut_doc);
14070 sessions = mut_root != NULL ? yyjson_mut_obj_get(mut_root, "sessions") : NULL;
14071 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root) || sessions == NULL || !yyjson_mut_is_arr(sessions)) {
14073 yyjson_mut_doc_free(mut_doc);
14074 return 0;
14075 }
14076 yyjson_mut_arr_foreach(sessions, idx, max, session) {
14077 yyjson_mut_val *hash_value = NULL;
14078 yyjson_mut_val *revoked_value = NULL;
14079 if (!yyjson_mut_is_obj(session)) {
14080 continue;
14081 }
14082 hash_value = yyjson_mut_obj_get(session, "session_token_hash");
14083 revoked_value = yyjson_mut_obj_get(session, "revoked_at");
14084 if (hash_value != NULL && yyjson_is_str((yyjson_val *)hash_value) && yyjson_equals_str((yyjson_val *)hash_value, session_token_hash) && (revoked_value == NULL || yyjson_is_null((yyjson_val *)revoked_value) || (yyjson_is_str((yyjson_val *)revoked_value) && yyjson_get_len((yyjson_val *)revoked_value) == 0))) {
14085 if (!yyjson_mut_obj_put(session, yyjson_mut_strcpy(mut_doc, "revoked_at"), yyjson_mut_strcpy(mut_doc, revoked_at))) {
14087 yyjson_mut_doc_free(mut_doc);
14088 return 69;
14089 }
14090 }
14091 }
14092 code = write_mutable_json_doc_runtime(mut_doc, state_path);
14094 yyjson_mut_doc_free(mut_doc);
14095 return code;
14096 }
14097 if (streq(name, "issue-email-verification")) {
14098 const char *user_id_text = json_option_value_runtime(argc, argv, "--user-id");
14099 const char *token_hash = json_option_value_runtime(argc, argv, "--token-hash");
14100 const char *created_at = json_option_value_runtime(argc, argv, "--created-at");
14101 const char *expires_at = json_option_value_runtime(argc, argv, "--expires-at");
14102 yyjson_mut_doc *mut_doc = NULL;
14103 yyjson_mut_val *mut_root = NULL;
14104 yyjson_mut_val *verifications = NULL;
14105 yyjson_mut_val *verification_obj = NULL;
14106 long user_id = strtol(user_id_text != NULL ? user_id_text : "0", NULL, 10);
14107 long verification_id = max_id_in_state_section_runtime(root, "email_verifications") + 1;
14108 size_t idx = 0;
14109 int code = 69;
14110 if (user_id <= 0 || token_hash == NULL || token_hash[0] == '\0' || created_at == NULL || created_at[0] == '\0' || expires_at == NULL || expires_at[0] == '\0') {
14112 return 64;
14113 }
14114 mut_doc = yyjson_doc_mut_copy(doc, NULL);
14115 mut_root = yyjson_mut_doc_get_root(mut_doc);
14116 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
14118 yyjson_mut_doc_free(mut_doc);
14119 return 69;
14120 }
14121 verifications = ensure_mut_array_field_runtime(mut_doc, mut_root, "email_verifications");
14122 if (verifications == NULL) {
14124 yyjson_mut_doc_free(mut_doc);
14125 return 69;
14126 }
14127 for (idx = yyjson_mut_arr_size(verifications); idx > 0; idx--) {
14128 yyjson_mut_val *item = yyjson_mut_arr_get(verifications, idx - 1);
14129 yyjson_mut_val *claimed_at_value = NULL;
14130 if (!yyjson_mut_is_obj(item)) {
14131 continue;
14132 }
14133 claimed_at_value = yyjson_mut_obj_get(item, "claimed_at");
14134 if (value_long_equals_runtime((yyjson_val *)yyjson_mut_obj_get(item, "user_id"), user_id) && (claimed_at_value == NULL || yyjson_is_null((yyjson_val *)claimed_at_value) || (yyjson_is_str((yyjson_val *)claimed_at_value) && yyjson_get_len((yyjson_val *)claimed_at_value) == 0))) {
14135 yyjson_mut_arr_remove(verifications, idx - 1);
14136 }
14137 }
14138 verification_obj = yyjson_mut_obj(mut_doc);
14139 if (verification_obj == NULL || !yyjson_mut_obj_add_int(mut_doc, verification_obj, "id", verification_id) || !yyjson_mut_obj_add_int(mut_doc, verification_obj, "user_id", user_id) || !yyjson_mut_obj_add_strcpy(mut_doc, verification_obj, "token_hash", token_hash) || !yyjson_mut_obj_add_strcpy(mut_doc, verification_obj, "expires_at", expires_at) || !yyjson_mut_obj_put(verification_obj, yyjson_mut_strcpy(mut_doc, "claimed_at"), yyjson_mut_null(mut_doc)) || !yyjson_mut_obj_add_strcpy(mut_doc, verification_obj, "created_at", created_at) || !yyjson_mut_arr_append(verifications, verification_obj)) {
14141 yyjson_mut_doc_free(mut_doc);
14142 return 69;
14143 }
14144 code = write_mutable_json_doc_runtime(mut_doc, state_path);
14146 yyjson_mut_doc_free(mut_doc);
14147 return code;
14148 }
14149 if (streq(name, "active-email-verification-json")) {
14150 const char *token_hash = json_option_value_runtime(argc, argv, "--token-hash");
14151 const char *now_iso = json_option_value_runtime(argc, argv, "--now-iso");
14152 int code = emit_json_or_empty_runtime(find_active_token_record_runtime(state_array_runtime(root, "email_verifications"), token_hash, now_iso));
14154 return code;
14155 }
14156 if (streq(name, "claim-email-verification")) {
14157 const char *verification_id_text = json_option_value_runtime(argc, argv, "--verification-id");
14158 const char *user_id_text = json_option_value_runtime(argc, argv, "--user-id");
14159 const char *claimed_at = json_option_value_runtime(argc, argv, "--claimed-at");
14160 yyjson_mut_doc *mut_doc = NULL;
14161 yyjson_mut_val *mut_root = NULL;
14162 yyjson_mut_val *users = NULL;
14163 yyjson_mut_val *verifications = NULL;
14164 yyjson_mut_val *user = NULL;
14165 yyjson_mut_val *verification = NULL;
14166 size_t idx = 0;
14167 size_t max = 0;
14168 long verification_id = strtol(verification_id_text != NULL ? verification_id_text : "0", NULL, 10);
14169 long user_id = strtol(user_id_text != NULL ? user_id_text : "0", NULL, 10);
14170 int code = 69;
14171 if (verification_id <= 0 || user_id <= 0 || claimed_at == NULL || claimed_at[0] == '\0') {
14173 return 64;
14174 }
14175 mut_doc = yyjson_doc_mut_copy(doc, NULL);
14176 mut_root = yyjson_mut_doc_get_root(mut_doc);
14177 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
14179 yyjson_mut_doc_free(mut_doc);
14180 return 69;
14181 }
14182 users = ensure_mut_array_field_runtime(mut_doc, mut_root, "users");
14183 verifications = ensure_mut_array_field_runtime(mut_doc, mut_root, "email_verifications");
14184 if (users == NULL || verifications == NULL) {
14186 yyjson_mut_doc_free(mut_doc);
14187 return 69;
14188 }
14189 yyjson_mut_arr_foreach(users, idx, max, user) {
14190 if (!yyjson_mut_is_obj(user)) {
14191 continue;
14192 }
14193 if (value_long_equals_runtime((yyjson_val *)yyjson_mut_obj_get(user, "id"), user_id)) {
14194 if (!yyjson_mut_obj_put(user, yyjson_mut_strcpy(mut_doc, "email_verified"), yyjson_mut_bool(mut_doc, 1))) {
14196 yyjson_mut_doc_free(mut_doc);
14197 return 69;
14198 }
14199 }
14200 }
14201 yyjson_mut_arr_foreach(verifications, idx, max, verification) {
14202 if (!yyjson_mut_is_obj(verification)) {
14203 continue;
14204 }
14205 if (value_long_equals_runtime((yyjson_val *)yyjson_mut_obj_get(verification, "id"), verification_id)) {
14206 if (!yyjson_mut_obj_put(verification, yyjson_mut_strcpy(mut_doc, "claimed_at"), yyjson_mut_strcpy(mut_doc, claimed_at))) {
14208 yyjson_mut_doc_free(mut_doc);
14209 return 69;
14210 }
14211 }
14212 }
14213 code = write_mutable_json_doc_runtime(mut_doc, state_path);
14214 if (code == 0) {
14215 fprintf(stdout, "{\"verification_id\":%ld,\"user_id\":%ld,\"claimed_at\":", verification_id, user_id);
14216 {
14217 yyjson_mut_val *claimed_at_val = yyjson_mut_strcpy(mut_doc, claimed_at);
14218 char *claimed_at_json = claimed_at_val != NULL ? yyjson_mut_val_write(claimed_at_val, YYJSON_WRITE_NOFLAG, NULL) : NULL;
14219 fputs(claimed_at_json != NULL ? claimed_at_json : "\"\"", stdout);
14220 free(claimed_at_json);
14221 }
14222 fputc('}', stdout);
14223 }
14225 yyjson_mut_doc_free(mut_doc);
14226 return code;
14227 }
14228 if (streq(name, "issue-password-reset")) {
14229 const char *user_id_text = json_option_value_runtime(argc, argv, "--user-id");
14230 const char *token_hash = json_option_value_runtime(argc, argv, "--token-hash");
14231 const char *created_at = json_option_value_runtime(argc, argv, "--created-at");
14232 const char *expires_at = json_option_value_runtime(argc, argv, "--expires-at");
14233 yyjson_mut_doc *mut_doc = NULL;
14234 yyjson_mut_val *mut_root = NULL;
14235 yyjson_mut_val *resets = NULL;
14236 yyjson_mut_val *reset_obj = NULL;
14237 long user_id = strtol(user_id_text != NULL ? user_id_text : "0", NULL, 10);
14238 long reset_id = max_id_in_state_section_runtime(root, "password_resets") + 1;
14239 size_t idx = 0;
14240 int code = 69;
14241 if (user_id <= 0 || token_hash == NULL || token_hash[0] == '\0' || created_at == NULL || created_at[0] == '\0' || expires_at == NULL || expires_at[0] == '\0') {
14243 return 64;
14244 }
14245 mut_doc = yyjson_doc_mut_copy(doc, NULL);
14246 mut_root = yyjson_mut_doc_get_root(mut_doc);
14247 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
14249 yyjson_mut_doc_free(mut_doc);
14250 return 69;
14251 }
14252 resets = ensure_mut_array_field_runtime(mut_doc, mut_root, "password_resets");
14253 if (resets == NULL) {
14255 yyjson_mut_doc_free(mut_doc);
14256 return 69;
14257 }
14258 for (idx = yyjson_mut_arr_size(resets); idx > 0; idx--) {
14259 yyjson_mut_val *item = yyjson_mut_arr_get(resets, idx - 1);
14260 yyjson_mut_val *claimed_at_value = NULL;
14261 if (!yyjson_mut_is_obj(item)) {
14262 continue;
14263 }
14264 claimed_at_value = yyjson_mut_obj_get(item, "claimed_at");
14265 if (value_long_equals_runtime((yyjson_val *)yyjson_mut_obj_get(item, "user_id"), user_id) && (claimed_at_value == NULL || yyjson_is_null((yyjson_val *)claimed_at_value) || (yyjson_is_str((yyjson_val *)claimed_at_value) && yyjson_get_len((yyjson_val *)claimed_at_value) == 0))) {
14266 yyjson_mut_arr_remove(resets, idx - 1);
14267 }
14268 }
14269 reset_obj = yyjson_mut_obj(mut_doc);
14270 if (reset_obj == NULL || !yyjson_mut_obj_add_int(mut_doc, reset_obj, "id", reset_id) || !yyjson_mut_obj_add_int(mut_doc, reset_obj, "user_id", user_id) || !yyjson_mut_obj_add_strcpy(mut_doc, reset_obj, "token_hash", token_hash) || !yyjson_mut_obj_add_strcpy(mut_doc, reset_obj, "expires_at", expires_at) || !yyjson_mut_obj_put(reset_obj, yyjson_mut_strcpy(mut_doc, "claimed_at"), yyjson_mut_null(mut_doc)) || !yyjson_mut_obj_add_strcpy(mut_doc, reset_obj, "created_at", created_at) || !yyjson_mut_arr_append(resets, reset_obj)) {
14272 yyjson_mut_doc_free(mut_doc);
14273 return 69;
14274 }
14275 code = write_mutable_json_doc_runtime(mut_doc, state_path);
14277 yyjson_mut_doc_free(mut_doc);
14278 return code;
14279 }
14280 if (streq(name, "active-password-reset-json")) {
14281 const char *token_hash = json_option_value_runtime(argc, argv, "--token-hash");
14282 const char *now_iso = json_option_value_runtime(argc, argv, "--now-iso");
14283 int code = emit_json_or_empty_runtime(find_active_token_record_runtime(state_array_runtime(root, "password_resets"), token_hash, now_iso));
14285 return code;
14286 }
14287 if (streq(name, "apply-password-reset")) {
14288 const char *reset_id_text = json_option_value_runtime(argc, argv, "--reset-id");
14289 const char *user_id_text = json_option_value_runtime(argc, argv, "--user-id");
14290 const char *password_hash = json_option_value_runtime(argc, argv, "--password-hash");
14291 const char *claimed_at = json_option_value_runtime(argc, argv, "--claimed-at");
14292 yyjson_mut_doc *mut_doc = NULL;
14293 yyjson_mut_val *mut_root = NULL;
14294 yyjson_mut_val *users = NULL;
14295 yyjson_mut_val *resets = NULL;
14296 yyjson_mut_val *sessions = NULL;
14297 yyjson_mut_val *user = NULL;
14298 yyjson_mut_val *reset = NULL;
14299 yyjson_mut_val *session = NULL;
14300 size_t idx = 0;
14301 size_t max = 0;
14302 long reset_id = strtol(reset_id_text != NULL ? reset_id_text : "0", NULL, 10);
14303 long user_id = strtol(user_id_text != NULL ? user_id_text : "0", NULL, 10);
14304 int code = 69;
14305 if (reset_id <= 0 || user_id <= 0 || password_hash == NULL || password_hash[0] == '\0' || claimed_at == NULL || claimed_at[0] == '\0') {
14307 return 64;
14308 }
14309 mut_doc = yyjson_doc_mut_copy(doc, NULL);
14310 mut_root = yyjson_mut_doc_get_root(mut_doc);
14311 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
14313 yyjson_mut_doc_free(mut_doc);
14314 return 69;
14315 }
14316 users = ensure_mut_array_field_runtime(mut_doc, mut_root, "users");
14317 resets = ensure_mut_array_field_runtime(mut_doc, mut_root, "password_resets");
14318 sessions = ensure_mut_array_field_runtime(mut_doc, mut_root, "sessions");
14319 if (users == NULL || resets == NULL || sessions == NULL) {
14321 yyjson_mut_doc_free(mut_doc);
14322 return 69;
14323 }
14324 yyjson_mut_arr_foreach(users, idx, max, user) {
14325 if (!yyjson_mut_is_obj(user)) {
14326 continue;
14327 }
14328 if (value_long_equals_runtime((yyjson_val *)yyjson_mut_obj_get(user, "id"), user_id)) {
14329 if (!yyjson_mut_obj_put(user, yyjson_mut_strcpy(mut_doc, "password_hash"), yyjson_mut_strcpy(mut_doc, password_hash))) {
14331 yyjson_mut_doc_free(mut_doc);
14332 return 69;
14333 }
14334 }
14335 }
14336 yyjson_mut_arr_foreach(resets, idx, max, reset) {
14337 if (!yyjson_mut_is_obj(reset)) {
14338 continue;
14339 }
14340 if (value_long_equals_runtime((yyjson_val *)yyjson_mut_obj_get(reset, "user_id"), user_id)) {
14341 if (!yyjson_mut_obj_put(reset, yyjson_mut_strcpy(mut_doc, "claimed_at"), yyjson_mut_strcpy(mut_doc, claimed_at))) {
14343 yyjson_mut_doc_free(mut_doc);
14344 return 69;
14345 }
14346 }
14347 }
14348 yyjson_mut_arr_foreach(sessions, idx, max, session) {
14349 yyjson_mut_val *revoked_value = NULL;
14350 if (!yyjson_mut_is_obj(session)) {
14351 continue;
14352 }
14353 if (!value_long_equals_runtime((yyjson_val *)yyjson_mut_obj_get(session, "user_id"), user_id)) {
14354 continue;
14355 }
14356 revoked_value = yyjson_mut_obj_get(session, "revoked_at");
14357 if (revoked_value == NULL || yyjson_is_null((yyjson_val *)revoked_value) || (yyjson_is_str((yyjson_val *)revoked_value) && yyjson_get_len((yyjson_val *)revoked_value) == 0)) {
14358 if (!yyjson_mut_obj_put(session, yyjson_mut_strcpy(mut_doc, "revoked_at"), yyjson_mut_strcpy(mut_doc, claimed_at))) {
14360 yyjson_mut_doc_free(mut_doc);
14361 return 69;
14362 }
14363 }
14364 }
14365 code = write_mutable_json_doc_runtime(mut_doc, state_path);
14366 if (code == 0) {
14367 fprintf(stdout, "{\"reset_id\":%ld,\"user_id\":%ld,\"claimed_at\":", reset_id, user_id);
14368 {
14369 yyjson_mut_val *claimed_at_val = yyjson_mut_strcpy(mut_doc, claimed_at);
14370 char *claimed_at_json = claimed_at_val != NULL ? yyjson_mut_val_write(claimed_at_val, YYJSON_WRITE_NOFLAG, NULL) : NULL;
14371 fputs(claimed_at_json != NULL ? claimed_at_json : "\"\"", stdout);
14372 free(claimed_at_json);
14373 }
14374 fputc('}', stdout);
14375 }
14377 yyjson_mut_doc_free(mut_doc);
14378 return code;
14379 }
14380 if (streq(name, "issue-invite")) {
14381 const char *email_value = json_option_value_runtime(argc, argv, "--email");
14382 const char *account_type_value = json_option_value_runtime(argc, argv, "--account-type");
14383 const char *invited_by_user_id_text = json_option_value_runtime(argc, argv, "--invited-by-user-id");
14384 const char *invite_kind_value = json_option_value_runtime(argc, argv, "--invite-kind");
14385 const char *target_tenant_id_text = json_option_value_runtime(argc, argv, "--target-tenant-id");
14386 const char *role_hint_value = json_option_value_runtime(argc, argv, "--role-hint");
14387 const char *token_hash = json_option_value_runtime(argc, argv, "--token-hash");
14388 const char *expires_at = json_option_value_runtime(argc, argv, "--expires-at");
14389 const char *created_at = json_option_value_runtime(argc, argv, "--created-at");
14390 yyjson_mut_doc *mut_doc = NULL;
14391 yyjson_mut_val *mut_root = NULL;
14392 yyjson_mut_val *invites = NULL;
14393 yyjson_mut_val *invite_obj = NULL;
14394 long invite_id = max_id_in_state_section_runtime(root, "invites") + 1;
14395 long invited_by_user_id = strtol(invited_by_user_id_text != NULL ? invited_by_user_id_text : "0", NULL, 10);
14396 long target_tenant_id = strtol(target_tenant_id_text != NULL ? target_tenant_id_text : "0", NULL, 10);
14397 size_t idx = 0;
14398 int code = 69;
14399 if (email_value == NULL || email_value[0] == '\0' || account_type_value == NULL || account_type_value[0] == '\0' || invite_kind_value == NULL || invite_kind_value[0] == '\0' || token_hash == NULL || token_hash[0] == '\0' || expires_at == NULL || expires_at[0] == '\0' || created_at == NULL || created_at[0] == '\0') {
14401 return 64;
14402 }
14403 mut_doc = yyjson_doc_mut_copy(doc, NULL);
14404 mut_root = yyjson_mut_doc_get_root(mut_doc);
14405 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
14407 yyjson_mut_doc_free(mut_doc);
14408 return 69;
14409 }
14410 invites = ensure_mut_array_field_runtime(mut_doc, mut_root, "invites");
14411 if (invites == NULL) {
14413 yyjson_mut_doc_free(mut_doc);
14414 return 69;
14415 }
14416 for (idx = yyjson_mut_arr_size(invites); idx > 0; idx--) {
14417 yyjson_mut_val *item = yyjson_mut_arr_get(invites, idx - 1);
14418 yyjson_mut_val *claimed_at_value = NULL;
14419 char *item_email = NULL;
14420 char *item_kind = NULL;
14421 int match = 0;
14422 if (!yyjson_mut_is_obj(item)) {
14423 continue;
14424 }
14425 claimed_at_value = yyjson_mut_obj_get(item, "claimed_at");
14426 item_email = native_json_obj_get_string_dup((yyjson_val *)item, "email");
14427 item_kind = native_json_obj_get_string_dup((yyjson_val *)item, "invite_kind");
14428 match = item_email != NULL && item_kind != NULL && streq_casefold_runtime(item_email, email_value) && streq(item_kind, invite_kind_value) && (claimed_at_value == NULL || yyjson_is_null((yyjson_val *)claimed_at_value) || (yyjson_is_str((yyjson_val *)claimed_at_value) && yyjson_get_len((yyjson_val *)claimed_at_value) == 0));
14429 free(item_email);
14430 free(item_kind);
14431 if (match) {
14432 yyjson_mut_arr_remove(invites, idx - 1);
14433 }
14434 }
14435 invite_obj = yyjson_mut_obj(mut_doc);
14436 if (invite_obj == NULL || !yyjson_mut_obj_add_int(mut_doc, invite_obj, "id", invite_id) || !yyjson_mut_obj_add_strcpy(mut_doc, invite_obj, "email", email_value) || !yyjson_mut_obj_add_strcpy(mut_doc, invite_obj, "account_type", account_type_value) || !yyjson_mut_obj_add_strcpy(mut_doc, invite_obj, "invite_kind", invite_kind_value) || !yyjson_mut_obj_add_strcpy(mut_doc, invite_obj, "token_hash", token_hash) || !yyjson_mut_obj_add_strcpy(mut_doc, invite_obj, "expires_at", expires_at) || !yyjson_mut_obj_put(invite_obj, yyjson_mut_strcpy(mut_doc, "claimed_at"), yyjson_mut_null(mut_doc)) || !yyjson_mut_obj_add_strcpy(mut_doc, invite_obj, "created_at", created_at)) {
14438 yyjson_mut_doc_free(mut_doc);
14439 return 69;
14440 }
14441 if (!yyjson_mut_obj_put(invite_obj, yyjson_mut_strcpy(mut_doc, "target_tenant_id"), target_tenant_id > 0 ? yyjson_mut_int(mut_doc, target_tenant_id) : yyjson_mut_null(mut_doc)) || !yyjson_mut_obj_put(invite_obj, yyjson_mut_strcpy(mut_doc, "role_hint"), (role_hint_value != NULL && role_hint_value[0] != '\0') ? yyjson_mut_strcpy(mut_doc, role_hint_value) : yyjson_mut_null(mut_doc)) || !yyjson_mut_obj_put(invite_obj, yyjson_mut_strcpy(mut_doc, "invited_by_user_id"), invited_by_user_id > 0 ? yyjson_mut_int(mut_doc, invited_by_user_id) : yyjson_mut_null(mut_doc)) || !yyjson_mut_obj_put(invite_obj, yyjson_mut_strcpy(mut_doc, "accepted_user_id"), yyjson_mut_null(mut_doc)) || !yyjson_mut_arr_append(invites, invite_obj)) {
14443 yyjson_mut_doc_free(mut_doc);
14444 return 69;
14445 }
14446 code = write_mutable_json_doc_runtime(mut_doc, state_path);
14448 yyjson_mut_doc_free(mut_doc);
14449 return code;
14450 }
14451 if (streq(name, "active-invite-json")) {
14452 const char *token_hash = json_option_value_runtime(argc, argv, "--token-hash");
14453 const char *now_iso = json_option_value_runtime(argc, argv, "--now-iso");
14454 int code = emit_json_or_empty_runtime(find_active_token_record_runtime(state_array_runtime(root, "invites"), token_hash, now_iso));
14456 return code;
14457 }
14458 if (streq(name, "claim-invite")) {
14459 const char *invite_id_text = json_option_value_runtime(argc, argv, "--invite-id");
14460 const char *accepted_user_id_text = json_option_value_runtime(argc, argv, "--accepted-user-id");
14461 const char *claimed_at = json_option_value_runtime(argc, argv, "--claimed-at");
14462 yyjson_mut_doc *mut_doc = NULL;
14463 yyjson_mut_val *mut_root = NULL;
14464 yyjson_mut_val *invites = NULL;
14465 yyjson_mut_val *invite = NULL;
14466 size_t idx = 0;
14467 size_t max = 0;
14468 long invite_id = strtol(invite_id_text != NULL ? invite_id_text : "0", NULL, 10);
14469 long accepted_user_id = strtol(accepted_user_id_text != NULL ? accepted_user_id_text : "0", NULL, 10);
14470 int code = 69;
14471 if (invite_id <= 0 || accepted_user_id <= 0 || claimed_at == NULL || claimed_at[0] == '\0') {
14473 return 64;
14474 }
14475 mut_doc = yyjson_doc_mut_copy(doc, NULL);
14476 mut_root = yyjson_mut_doc_get_root(mut_doc);
14477 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
14479 yyjson_mut_doc_free(mut_doc);
14480 return 69;
14481 }
14482 invites = ensure_mut_array_field_runtime(mut_doc, mut_root, "invites");
14483 if (invites == NULL) {
14485 yyjson_mut_doc_free(mut_doc);
14486 return 69;
14487 }
14488 yyjson_mut_arr_foreach(invites, idx, max, invite) {
14489 if (!yyjson_mut_is_obj(invite)) {
14490 continue;
14491 }
14492 if (value_long_equals_runtime((yyjson_val *)yyjson_mut_obj_get(invite, "id"), invite_id)) {
14493 if (!yyjson_mut_obj_put(invite, yyjson_mut_strcpy(mut_doc, "accepted_user_id"), yyjson_mut_int(mut_doc, accepted_user_id)) || !yyjson_mut_obj_put(invite, yyjson_mut_strcpy(mut_doc, "claimed_at"), yyjson_mut_strcpy(mut_doc, claimed_at))) {
14495 yyjson_mut_doc_free(mut_doc);
14496 return 69;
14497 }
14498 }
14499 }
14500 code = write_mutable_json_doc_runtime(mut_doc, state_path);
14501 if (code == 0) {
14502 fprintf(stdout, "{\"invite_id\":%ld,\"accepted_user_id\":%ld,\"claimed_at\":", invite_id, accepted_user_id);
14503 {
14504 yyjson_mut_val *claimed_at_val = yyjson_mut_strcpy(mut_doc, claimed_at);
14505 char *claimed_at_json = claimed_at_val != NULL ? yyjson_mut_val_write(claimed_at_val, YYJSON_WRITE_NOFLAG, NULL) : NULL;
14506 fputs(claimed_at_json != NULL ? claimed_at_json : "\"\"", stdout);
14507 free(claimed_at_json);
14508 }
14509 fputc('}', stdout);
14510 }
14512 yyjson_mut_doc_free(mut_doc);
14513 return code;
14514 }
14515 if (streq(name, "authenticate-user-json")) {
14516 const char *username = json_option_value_runtime(argc, argv, "--username");
14517 yyjson_val *user = state_find_user_by_username_runtime(root, username);
14518 if (user != NULL) {
14519 yyjson_val *active_value = native_json_obj_get(user, "is_active");
14520 if (yyjson_is_bool(active_value) && !yyjson_get_bool(active_value)) {
14522 return 0;
14523 }
14524 }
14525 {
14526 int code = emit_json_or_empty_runtime(user);
14528 return code;
14529 }
14530 }
14531 if (streq(name, "active-session-json")) {
14532 const char *session_token_hash = json_option_value_runtime(argc, argv, "--session-token-hash");
14533 const char *now_iso = json_option_value_runtime(argc, argv, "--now-iso");
14534 yyjson_arr_iter iter;
14535 yyjson_val *sessions = state_array_runtime(root, "sessions");
14536 yyjson_val *item = NULL;
14537 if (sessions != NULL && yyjson_arr_iter_init(sessions, &iter)) {
14538 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
14539 yyjson_val *hash_value = native_json_obj_get(item, "session_token_hash");
14540 char *expires_at = NULL;
14541 char *revoked_at = NULL;
14542 int match = 0;
14543 if (!yyjson_is_obj(item) || !yyjson_is_str(hash_value) || session_token_hash == NULL || now_iso == NULL) {
14544 continue;
14545 }
14546 match = yyjson_equals_str(hash_value, session_token_hash);
14547 expires_at = native_json_obj_get_string_dup(item, "expires_at");
14548 revoked_at = native_json_obj_get_string_dup(item, "revoked_at");
14549 if (match && (revoked_at == NULL || revoked_at[0] == '\0') && expires_at != NULL && strcmp(expires_at, now_iso) > 0) {
14550 free(expires_at);
14551 free(revoked_at);
14552 {
14553 int code = emit_compact_json_value_runtime(item);
14555 return code;
14556 }
14557 }
14558 free(expires_at);
14559 free(revoked_at);
14560 }
14561 }
14563 return 0;
14564 }
14565 if (streq(name, "current-user-json")) {
14566 const char *session_token_hash = json_option_value_runtime(argc, argv, "--session-token-hash");
14567 const char *now_iso = json_option_value_runtime(argc, argv, "--now-iso");
14568 yyjson_arr_iter iter;
14569 yyjson_val *sessions = state_array_runtime(root, "sessions");
14570 yyjson_val *session = NULL;
14571 long user_id = 0;
14572 if (sessions != NULL && yyjson_arr_iter_init(sessions, &iter)) {
14573 while ((session = yyjson_arr_iter_next(&iter)) != NULL) {
14574 yyjson_val *hash_value = native_json_obj_get(session, "session_token_hash");
14575 char *expires_at = NULL;
14576 char *revoked_at = NULL;
14577 int match = 0;
14578 if (!yyjson_is_obj(session) || !yyjson_is_str(hash_value) || session_token_hash == NULL || now_iso == NULL) {
14579 continue;
14580 }
14581 match = yyjson_equals_str(hash_value, session_token_hash);
14582 expires_at = native_json_obj_get_string_dup(session, "expires_at");
14583 revoked_at = native_json_obj_get_string_dup(session, "revoked_at");
14584 if (match && (revoked_at == NULL || revoked_at[0] == '\0') && expires_at != NULL && strcmp(expires_at, now_iso) > 0) {
14585 user_id = native_json_obj_get_long_default(session, "user_id", 0, NULL);
14586 free(expires_at);
14587 free(revoked_at);
14588 break;
14589 }
14590 free(expires_at);
14591 free(revoked_at);
14592 }
14593 }
14594 if (user_id != 0) {
14595 yyjson_val *user = state_find_user_by_id_runtime(root, user_id);
14596 if (user != NULL) {
14597 yyjson_val *active_value = native_json_obj_get(user, "is_active");
14598 if (!(yyjson_is_bool(active_value) && !yyjson_get_bool(active_value))) {
14599 int code = emit_compact_json_value_runtime(user);
14601 return code;
14602 }
14603 }
14604 }
14606 return 0;
14607 }
14609 return 78;
14610}
14611
14612static char *url_decode_dup_runtime(const char *text) {
14613 size_t len = 0;
14614 size_t in_index = 0;
14615 size_t out_index = 0;
14616 char *out = NULL;
14617 if (text == NULL) {
14618 return strdup("");
14619 }
14620 len = strlen(text);
14621 out = (char *)malloc(len + 1);
14622 if (out == NULL) {
14623 return NULL;
14624 }
14625 while (in_index < len) {
14626 char ch = text[in_index];
14627 if (ch == '+') {
14628 out[out_index++] = ' ';
14629 in_index++;
14630 continue;
14631 }
14632 if (ch == '%' && in_index + 2 < len && isxdigit((unsigned char)text[in_index + 1]) && isxdigit((unsigned char)text[in_index + 2])) {
14633 char hex_text[3];
14634 unsigned int value = 0;
14635 hex_text[0] = text[in_index + 1];
14636 hex_text[1] = text[in_index + 2];
14637 hex_text[2] = '\0';
14638 sscanf(hex_text, "%x", &value);
14639 out[out_index++] = (char)value;
14640 in_index += 3;
14641 continue;
14642 }
14643 out[out_index++] = ch;
14644 in_index++;
14645 }
14646 out[out_index] = '\0';
14647 return out;
14648}
14649
14650static char *query_value_decoded_dup_runtime(const char *raw_query, const char *key) {
14651 char *copy = NULL;
14652 char *part = NULL;
14653 char *saveptr = NULL;
14654 size_t key_len = 0;
14655 if (raw_query == NULL || key == NULL || key[0] == '\0') {
14656 return NULL;
14657 }
14658 copy = strdup(raw_query);
14659 if (copy == NULL) {
14660 return NULL;
14661 }
14662 key_len = strlen(key);
14663 part = strtok_r(copy, "&", &saveptr);
14664 while (part != NULL) {
14665 char *eq = strchr(part, '=');
14666 if (eq != NULL && (size_t)(eq - part) == key_len && strncmp(part, key, key_len) == 0) {
14667 char *decoded = url_decode_dup_runtime(eq + 1);
14668 free(copy);
14669 return decoded;
14670 }
14671 part = strtok_r(NULL, "&", &saveptr);
14672 }
14673 free(copy);
14674 return NULL;
14675}
14676
14677static int route_template_match_into_mut_obj_runtime(yyjson_mut_doc *doc, const char *template_path, const char *request_path, yyjson_mut_val **out_params) {
14678 char *template_copy = NULL;
14679 char *request_copy = NULL;
14680 char *template_save = NULL;
14681 char *request_save = NULL;
14682 char *template_part = NULL;
14683 char *request_part = NULL;
14684 yyjson_mut_val *params = NULL;
14685 if (out_params != NULL) {
14686 *out_params = NULL;
14687 }
14688 if (doc == NULL || template_path == NULL || request_path == NULL || out_params == NULL) {
14689 return 0;
14690 }
14691 template_copy = strdup(template_path);
14692 request_copy = strdup(request_path);
14693 params = yyjson_mut_obj(doc);
14694 if (template_copy == NULL || request_copy == NULL || params == NULL) {
14695 free(template_copy);
14696 free(request_copy);
14697 return 0;
14698 }
14699 template_part = strtok_r(template_copy, "/", &template_save);
14700 request_part = strtok_r(request_copy, "/", &request_save);
14701 while (template_part != NULL || request_part != NULL) {
14702 size_t template_len = template_part != NULL ? strlen(template_part) : 0;
14703 if (template_part == NULL || request_part == NULL) {
14704 free(template_copy);
14705 free(request_copy);
14706 return 0;
14707 }
14708 if (template_len >= 2 && template_part[0] == '{' && template_part[template_len - 1] == '}') {
14709 char *name = dup_range(template_part + 1, template_len - 2);
14710 yyjson_mut_val *key_val = NULL;
14711 yyjson_mut_val *value_val = NULL;
14712 if (name == NULL) {
14713 free(template_copy);
14714 free(request_copy);
14715 return 0;
14716 }
14717 key_val = yyjson_mut_strcpy(doc, name);
14718 value_val = yyjson_mut_strcpy(doc, request_part);
14719 if (key_val == NULL || value_val == NULL || !yyjson_mut_obj_put(params, key_val, value_val)) {
14720 free(name);
14721 free(template_copy);
14722 free(request_copy);
14723 return 0;
14724 }
14725 free(name);
14726 } else if (!streq(template_part, request_part)) {
14727 free(template_copy);
14728 free(request_copy);
14729 return 0;
14730 }
14731 template_part = strtok_r(NULL, "/", &template_save);
14732 request_part = strtok_r(NULL, "/", &request_save);
14733 }
14734 free(template_copy);
14735 free(request_copy);
14736 *out_params = params;
14737 return 1;
14738}
14739
14740static int handle_file_generated_surface_match_command(int argc, char **argv) {
14741 const char *file_path = json_option_value_runtime(argc, argv, "--file");
14742 const char *method = json_option_value_runtime(argc, argv, "--method");
14743 const char *request_path = json_option_value_runtime(argc, argv, "--path");
14744 yyjson_doc *doc = NULL;
14745 yyjson_val *root = NULL;
14746 yyjson_val *surfaces = NULL;
14747 yyjson_arr_iter surface_iter;
14748 yyjson_val *surface = NULL;
14749 if (file_path == NULL || method == NULL || request_path == NULL) {
14750 return 64;
14751 }
14752 doc = native_json_doc_load_file(file_path);
14753 if (doc == NULL) {
14754 return 66;
14755 }
14756 root = yyjson_doc_get_root(doc);
14757 surfaces = native_json_obj_get_array(root, "surfaces");
14758 if (surfaces != NULL && yyjson_arr_iter_init(surfaces, &surface_iter)) {
14759 while ((surface = yyjson_arr_iter_next(&surface_iter)) != NULL) {
14760 yyjson_val *operations = NULL;
14761 yyjson_obj_iter op_iter;
14762 yyjson_val *op_key = NULL;
14763 char *surface_kind = native_json_obj_get_string_dup(surface, "surface_kind");
14764 if (surface_kind == NULL || (!streq(surface_kind, "generated_api") && !streq(surface_kind, "generated_admin"))) {
14765 free(surface_kind);
14766 continue;
14767 }
14768 free(surface_kind);
14769 operations = native_json_obj_get(surface, "operations");
14770 if (operations == NULL || !yyjson_is_obj(operations) || !yyjson_obj_iter_init(operations, &op_iter)) {
14771 continue;
14772 }
14773 while ((op_key = yyjson_obj_iter_next(&op_iter)) != NULL) {
14774 yyjson_val *op_value = yyjson_obj_iter_get_val(op_key);
14775 char *operation_name = NULL;
14776 char *operation_method = NULL;
14777 char *operation_path = NULL;
14778 yyjson_mut_doc *out_doc = NULL;
14779 yyjson_mut_val *out_root = NULL;
14780 yyjson_mut_val *params = NULL;
14781 int code = 0;
14782 if (!yyjson_is_str(op_key) || op_value == NULL || !yyjson_is_obj(op_value)) {
14783 continue;
14784 }
14785 operation_name = dup_range(yyjson_get_str(op_key), yyjson_get_len(op_key));
14786 operation_method = native_json_obj_get_string_dup(op_value, "method");
14787 operation_path = native_json_obj_get_string_dup(op_value, "path");
14788 if (operation_method == NULL || operation_method[0] == '\0') {
14789 free(operation_method);
14790 operation_method = strdup("GET");
14791 }
14792 if (operation_name == NULL || operation_method == NULL || operation_path == NULL || operation_path[0] == '\0' || !streq(operation_method, method)) {
14793 free(operation_name);
14794 free(operation_method);
14795 free(operation_path);
14796 continue;
14797 }
14798 out_doc = yyjson_mut_doc_new(NULL);
14799 out_root = out_doc != NULL ? yyjson_mut_obj(out_doc) : NULL;
14800 if (out_doc == NULL || out_root == NULL || !route_template_match_into_mut_obj_runtime(out_doc, operation_path, request_path, &params) || !yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "surface"), yyjson_val_mut_copy(out_doc, surface)) || !yyjson_mut_obj_add_strcpy(out_doc, out_root, "operation", operation_name) || !yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "operation_config"), yyjson_val_mut_copy(out_doc, op_value)) || !yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "params"), params)) {
14801 free(operation_name);
14802 free(operation_method);
14803 free(operation_path);
14804 yyjson_mut_doc_free(out_doc);
14805 continue;
14806 }
14807 yyjson_mut_doc_set_root(out_doc, out_root);
14808 code = emit_compact_json_mut_value_runtime(out_root);
14809 free(operation_name);
14810 free(operation_method);
14811 free(operation_path);
14812 yyjson_mut_doc_free(out_doc);
14814 return code;
14815 }
14816 }
14817 }
14819 return 66;
14820}
14821
14823 if (value == NULL || yyjson_is_null(value)) {
14824 return strdup("");
14825 }
14826 if (yyjson_is_str(value)) {
14827 return dup_range(yyjson_get_str(value), yyjson_get_len(value));
14828 }
14830}
14831
14832static int json_value_equals_text_runtime(yyjson_val *value, const char *expected) {
14833 char *actual = NULL;
14834 int match = 0;
14835 if (expected == NULL) {
14836 expected = "";
14837 }
14838 actual = json_value_tostring_dup_runtime(value);
14839 match = actual != NULL && streq(actual, expected);
14840 free(actual);
14841 return match;
14842}
14843
14844static int digits_only_text_runtime(const char *text) {
14845 size_t index = 0;
14846 if (text == NULL || text[0] == '\0') {
14847 return 0;
14848 }
14849 for (index = 0; text[index] != '\0'; index++) {
14850 if (!isdigit((unsigned char)text[index])) {
14851 return 0;
14852 }
14853 }
14854 return 1;
14855}
14856
14857static int contains_casefold_runtime(const char *haystack, const char *needle) {
14858 size_t hay_len = 0;
14859 size_t needle_len = 0;
14860 size_t start = 0;
14861 if (haystack == NULL || needle == NULL) {
14862 return 0;
14863 }
14864 hay_len = strlen(haystack);
14865 needle_len = strlen(needle);
14866 if (needle_len == 0) {
14867 return 1;
14868 }
14869 if (needle_len > hay_len) {
14870 return 0;
14871 }
14872 for (start = 0; start + needle_len <= hay_len; start++) {
14873 size_t idx = 0;
14874 int match = 1;
14875 for (idx = 0; idx < needle_len; idx++) {
14876 if (tolower((unsigned char)haystack[start + idx]) != tolower((unsigned char)needle[idx])) {
14877 match = 0;
14878 break;
14879 }
14880 }
14881 if (match) {
14882 return 1;
14883 }
14884 }
14885 return 0;
14886}
14887
14889 char *text = NULL;
14890 int truthy = 0;
14891 if (yyjson_is_bool(value)) {
14892 return yyjson_get_bool(value) ? 1 : 0;
14893 }
14894 if (yyjson_is_num(value)) {
14895 long numeric = 0;
14896 return long_from_value_runtime(value, &numeric) && numeric != 0;
14897 }
14898 text = json_value_tostring_dup_runtime(value);
14899 truthy = text != NULL && (streq_casefold_runtime(text, "1") || streq_casefold_runtime(text, "true") || streq_casefold_runtime(text, "yes") || streq_casefold_runtime(text, "on"));
14900 free(text);
14901 return truthy;
14902}
14903
14904static int weekday_rank_runtime(const char *value) {
14905 if (value == NULL) {
14906 return 99;
14907 }
14908 if (streq(value, "Mon")) return 1;
14909 if (streq(value, "Tue")) return 2;
14910 if (streq(value, "Wed")) return 3;
14911 if (streq(value, "Thu")) return 4;
14912 if (streq(value, "Fri")) return 5;
14913 if (streq(value, "Sat")) return 6;
14914 if (streq(value, "Sun")) return 7;
14915 return 99;
14916}
14917
14918static yyjson_val *surface_find_by_id_runtime(yyjson_val *root, const char *surface_id) {
14919 yyjson_val *surfaces = native_json_obj_get_array(root, "surfaces");
14920 yyjson_arr_iter iter;
14921 yyjson_val *item = NULL;
14922 if (surfaces == NULL || surface_id == NULL || !yyjson_arr_iter_init(surfaces, &iter)) {
14923 return NULL;
14924 }
14925 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
14926 if (yyjson_equals_str(native_json_obj_get(item, "surface_id"), surface_id)) {
14927 return item;
14928 }
14929 }
14930 return NULL;
14931}
14932
14933static yyjson_val *surface_find_form_runtime(yyjson_val *root, const char *form_id) {
14934 yyjson_val *forms = native_json_obj_get_array(root, "forms");
14935 yyjson_arr_iter iter;
14936 yyjson_val *item = NULL;
14937 if (forms == NULL || form_id == NULL || !yyjson_arr_iter_init(forms, &iter)) {
14938 return NULL;
14939 }
14940 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
14941 if (yyjson_equals_str(native_json_obj_get(item, "form_id"), form_id)) {
14942 return item;
14943 }
14944 }
14945 return NULL;
14946}
14947
14948static yyjson_val *surface_find_record_by_string_field_runtime(yyjson_val *root, const char *collection_name, const char *field_name, const char *expected_value) {
14949 yyjson_val *rows = state_array_runtime(root, collection_name);
14950 yyjson_arr_iter iter;
14951 yyjson_val *item = NULL;
14952 if (rows == NULL || field_name == NULL || expected_value == NULL || !yyjson_arr_iter_init(rows, &iter)) {
14953 return NULL;
14954 }
14955 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
14956 if (json_value_equals_text_runtime(native_json_obj_get(item, field_name), expected_value)) {
14957 return item;
14958 }
14959 }
14960 return NULL;
14961}
14962
14963static int surface_context_match_runtime(yyjson_val *item, const char *scope_field, yyjson_val *context_id_value) {
14964 yyjson_val *field_value = NULL;
14965 long lhs = 0;
14966 long rhs = 0;
14967 if (scope_field == NULL || scope_field[0] == '\0' || context_id_value == NULL || yyjson_is_null(context_id_value)) {
14968 return 1;
14969 }
14970 field_value = native_json_obj_get(item, scope_field);
14971 if (long_from_value_runtime(field_value, &lhs) && long_from_value_runtime(context_id_value, &rhs)) {
14972 return lhs == rhs;
14973 }
14974 {
14975 char *left = json_value_tostring_dup_runtime(field_value);
14976 char *right = json_value_tostring_dup_runtime(context_id_value);
14977 int match = left != NULL && right != NULL && streq(left, right);
14978 free(left);
14979 free(right);
14980 return match;
14981 }
14982}
14983
14985 yyjson_arr_iter iter;
14986 yyjson_val *filter = NULL;
14987 if (filters == NULL || !yyjson_is_arr(filters) || !yyjson_arr_iter_init(filters, &iter)) {
14988 return 1;
14989 }
14990 while ((filter = yyjson_arr_iter_next(&iter)) != NULL) {
14991 char *value = native_json_obj_get_string_dup(filter, "value");
14992 char *field = native_json_obj_get_string_dup(filter, "field");
14993 char *match_kind = native_json_obj_get_string_dup(filter, "match");
14994 yyjson_val *item_value = NULL;
14995 int matched = 1;
14996 if (value == NULL || value[0] == '\0') {
14997 free(value);
14998 free(field);
14999 free(match_kind);
15000 continue;
15001 }
15002 if (field == NULL || field[0] == '\0') {
15003 free(value);
15004 free(field);
15005 free(match_kind);
15006 continue;
15007 }
15008 item_value = native_json_obj_get(item, field);
15009 if (match_kind != NULL && streq(match_kind, "contains")) {
15010 char *item_text = json_value_tostring_dup_runtime(item_value);
15011 matched = item_text != NULL && contains_casefold_runtime(item_text, value);
15012 free(item_text);
15013 } else if (match_kind != NULL && streq(match_kind, "boolean")) {
15014 if (streq_casefold_runtime(value, "all") || value[0] == '\0') {
15015 matched = 1;
15016 } else {
15017 int want_true = streq_casefold_runtime(value, "true") || streq_casefold_runtime(value, "1") || streq_casefold_runtime(value, "yes") || streq_casefold_runtime(value, "on");
15018 int want_false = streq_casefold_runtime(value, "false") || streq_casefold_runtime(value, "0") || streq_casefold_runtime(value, "no") || streq_casefold_runtime(value, "off");
15019 matched = (!want_true && !want_false) || (want_true && json_value_truthy_runtime(item_value)) || (want_false && !json_value_truthy_runtime(item_value));
15020 }
15021 } else if (match_kind != NULL && streq(match_kind, "array_contains_integer")) {
15022 long wanted = 0;
15023 matched = 1;
15024 if (digits_only_text_runtime(value)) {
15025 yyjson_val *array_value = item_value;
15026 yyjson_arr_iter array_iter;
15027 yyjson_val *entry = NULL;
15028 matched = 0;
15029 wanted = strtol(value, NULL, 10);
15030 if (array_value != NULL && yyjson_is_arr(array_value) && yyjson_arr_iter_init(array_value, &array_iter)) {
15031 while ((entry = yyjson_arr_iter_next(&array_iter)) != NULL) {
15032 long actual = 0;
15033 if (long_from_value_runtime(entry, &actual) && actual == wanted) {
15034 matched = 1;
15035 break;
15036 }
15037 }
15038 }
15039 }
15040 } else {
15041 char *item_text = json_value_tostring_dup_runtime(item_value);
15042 matched = item_text != NULL && streq(item_text, value);
15043 free(item_text);
15044 }
15045 free(value);
15046 free(field);
15047 free(match_kind);
15048 if (!matched) {
15049 return 0;
15050 }
15051 }
15052 return 1;
15053}
15054
15055static int surface_compare_sort_values_runtime(yyjson_val *left_item, yyjson_val *right_item, const char *field_name, const char *type_name) {
15056 yyjson_val *left_value = NULL;
15057 yyjson_val *right_value = NULL;
15058 if (field_name == NULL || field_name[0] == '\0') {
15059 return 0;
15060 }
15061 left_value = native_json_obj_get(left_item, field_name);
15062 right_value = native_json_obj_get(right_item, field_name);
15063 if (type_name != NULL && streq(type_name, "number")) {
15064 long left_number = 0;
15065 long right_number = 0;
15066 long_from_value_runtime(left_value, &left_number);
15067 long_from_value_runtime(right_value, &right_number);
15068 if (left_number < right_number) return -1;
15069 if (left_number > right_number) return 1;
15070 return 0;
15071 }
15072 if (type_name != NULL && streq(type_name, "weekday")) {
15073 char *left_text = json_value_tostring_dup_runtime(left_value);
15074 char *right_text = json_value_tostring_dup_runtime(right_value);
15075 int left_rank = weekday_rank_runtime(left_text);
15076 int right_rank = weekday_rank_runtime(right_text);
15077 free(left_text);
15078 free(right_text);
15079 if (left_rank < right_rank) return -1;
15080 if (left_rank > right_rank) return 1;
15081 return 0;
15082 }
15083 {
15084 char *left_text = json_value_tostring_dup_runtime(left_value);
15085 char *right_text = json_value_tostring_dup_runtime(right_value);
15086 int cmp = 0;
15087 if (left_text == NULL) left_text = strdup("");
15088 if (right_text == NULL) right_text = strdup("");
15089 cmp = strcasecmp(left_text, right_text);
15090 free(left_text);
15091 free(right_text);
15092 if (cmp < 0) return -1;
15093 if (cmp > 0) return 1;
15094 return 0;
15095 }
15096}
15097
15099 size_t i = 0;
15100 char *field_name = NULL;
15101 char *direction = NULL;
15102 char *type_name = NULL;
15103 char *secondary_field = NULL;
15104 char *secondary_type = NULL;
15105 if (list == NULL || list->count < 2 || sort_config == NULL || !yyjson_is_obj(sort_config)) {
15106 return;
15107 }
15108 field_name = native_json_obj_get_string_dup(sort_config, "field");
15109 direction = native_json_obj_get_string_dup(sort_config, "direction");
15110 type_name = native_json_obj_get_string_dup(sort_config, "type");
15111 secondary_field = native_json_obj_get_string_dup(sort_config, "secondary_field");
15112 secondary_type = native_json_obj_get_string_dup(sort_config, "secondary_type");
15113 if (field_name == NULL || field_name[0] == '\0') {
15114 free(field_name);
15115 free(direction);
15116 free(type_name);
15117 free(secondary_field);
15118 free(secondary_type);
15119 return;
15120 }
15121 for (i = 1; i < list->count; i++) {
15122 size_t j = i;
15123 while (j > 0) {
15124 int cmp = surface_compare_sort_values_runtime(list->items[j - 1], list->items[j], field_name, type_name != NULL && type_name[0] != '\0' ? type_name : "string");
15125 if (cmp == 0 && secondary_field != NULL && secondary_field[0] != '\0') {
15126 cmp = surface_compare_sort_values_runtime(list->items[j - 1], list->items[j], secondary_field, secondary_type != NULL && secondary_type[0] != '\0' ? secondary_type : "string");
15127 }
15128 if (cmp == 0) {
15129 long left_id = native_json_obj_get_long_default(list->items[j - 1], "id", 0, NULL);
15130 long right_id = native_json_obj_get_long_default(list->items[j], "id", 0, NULL);
15131 if (left_id < right_id) cmp = -1;
15132 else if (left_id > right_id) cmp = 1;
15133 }
15134 if (direction != NULL && streq_casefold_runtime(direction, "desc")) {
15135 cmp = -cmp;
15136 }
15137 if (cmp <= 0) {
15138 break;
15139 }
15140 {
15141 yyjson_val *tmp = list->items[j - 1];
15142 list->items[j - 1] = list->items[j];
15143 list->items[j] = tmp;
15144 }
15145 j--;
15146 }
15147 }
15148 free(field_name);
15149 free(direction);
15150 free(type_name);
15151 free(secondary_field);
15152 free(secondary_type);
15153}
15154
15155static char *generated_surface_label_from_field_runtime(const char *field_name) {
15156 size_t i = 0;
15157 size_t len = 0;
15158 char *label = NULL;
15159 if (field_name == NULL) {
15160 return strdup("value");
15161 }
15162 len = strlen(field_name);
15163 label = (char *)malloc(len + 1);
15164 if (label == NULL) {
15165 return NULL;
15166 }
15167 for (i = 0; i < len; i++) {
15168 label[i] = field_name[i] == '_' ? ' ' : field_name[i];
15169 }
15170 label[len] = '\0';
15171 return label;
15172}
15173
15174static int generated_surface_report_add_field_error_runtime(yyjson_mut_doc *doc, yyjson_mut_val *report_root, const char *field_id, const char *rule_id, const char *code_text, const char *message) {
15175 yyjson_mut_val *errors = NULL;
15176 yyjson_mut_val *entry = NULL;
15177 if (doc == NULL || report_root == NULL || !yyjson_mut_is_obj(report_root)) {
15178 return 0;
15179 }
15180 errors = ensure_mut_array_field_runtime(doc, report_root, "field_errors");
15181 entry = yyjson_mut_obj(doc);
15182 if (errors == NULL || entry == NULL) {
15183 return 0;
15184 }
15185 if (field_id != NULL && field_id[0] != '\0' && !yyjson_mut_obj_add_strcpy(doc, entry, "field_id", field_id)) {
15186 return 0;
15187 }
15188 if (!yyjson_mut_obj_add_strcpy(doc, entry, "rule_id", rule_id != NULL ? rule_id : "") ||
15189 !yyjson_mut_obj_add_strcpy(doc, entry, "code", code_text != NULL ? code_text : "") ||
15190 !yyjson_mut_obj_add_strcpy(doc, entry, "message", message != NULL ? message : "") ||
15191 !yyjson_mut_arr_append(errors, entry)) {
15192 return 0;
15193 }
15194 return 1;
15195}
15196
15197static int generated_surface_array_contains_string_runtime(yyjson_val *array, const char *wanted) {
15198 yyjson_arr_iter iter;
15199 yyjson_val *item = NULL;
15200 if (array == NULL || wanted == NULL || !yyjson_is_arr(array) || !yyjson_arr_iter_init(array, &iter)) {
15201 return 0;
15202 }
15203 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
15204 if (yyjson_equals_str(item, wanted)) {
15205 return 1;
15206 }
15207 }
15208 return 0;
15209}
15210
15212 char *configured_strategy = native_json_obj_get_string_dup(surface_root, "delete_strategy");
15213 if (configured_strategy != NULL && configured_strategy[0] != '\0') {
15214 if (streq(configured_strategy, "soft") || streq(configured_strategy, "hard")) {
15215 return configured_strategy;
15216 }
15217 free(configured_strategy);
15218 return strdup("unsupported");
15219 }
15220 free(configured_strategy);
15221 {
15222 char *soft_delete_field = native_json_obj_get_string_dup(surface_root, "soft_delete_field");
15223 if (soft_delete_field != NULL && soft_delete_field[0] != '\0') {
15224 free(soft_delete_field);
15225 return strdup("soft");
15226 }
15227 free(soft_delete_field);
15228 }
15229 return strdup("unsupported");
15230}
15231
15237
15238static int compare_generated_surface_order_item_runtime(const void *left_ptr, const void *right_ptr) {
15241 if (left->sort_key < right->sort_key) return -1;
15242 if (left->sort_key > right->sort_key) return 1;
15243 if (left->record_id < right->record_id) return -1;
15244 if (left->record_id > right->record_id) return 1;
15245 return 0;
15246}
15247
15248static int generated_surface_normalize_order_mut_runtime(yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, yyjson_val *surface_root, yyjson_val *context_root) {
15249 char *sort_field = native_json_obj_get_string_dup(surface_root, "auto_sequence_field");
15250 char *collection_name = native_json_obj_get_string_dup(surface_root, "entity_collection");
15251 char *scope_field = native_json_obj_get_string_dup(surface_root, "scope_field");
15252 char *identity_field = native_json_obj_get_string_dup(surface_root, "identity_field");
15253 yyjson_mut_val *section = NULL;
15254 yyjson_val *context_id = native_json_obj_get(context_root, "context_id");
15256 size_t count = 0;
15257 size_t capacity = 0;
15258 size_t idx = 0;
15259 size_t max = 0;
15260 yyjson_mut_val *row = NULL;
15261 int ok = 1;
15262 if (sort_field == NULL || sort_field[0] == '\0' || collection_name == NULL || collection_name[0] == '\0' || scope_field == NULL || scope_field[0] == '\0') {
15263 free(sort_field);
15264 free(collection_name);
15265 free(scope_field);
15266 free(identity_field);
15267 return 1;
15268 }
15269 if (identity_field == NULL || identity_field[0] == '\0') {
15270 free(identity_field);
15271 identity_field = strdup("id");
15272 }
15273 section = yyjson_mut_obj_get(mut_root, collection_name);
15274 if (section == NULL || !yyjson_mut_is_arr(section) || context_id == NULL || yyjson_is_null(context_id)) {
15275 free(sort_field);
15276 free(collection_name);
15277 free(scope_field);
15278 free(identity_field);
15279 return 1;
15280 }
15281 yyjson_mut_arr_foreach(section, idx, max, row) {
15282 long sort_value = 0;
15283 long record_id = 0;
15284 yyjson_val *field_value = NULL;
15286 if (!yyjson_mut_is_obj(row) || !surface_context_match_runtime((yyjson_val *)row, scope_field, context_id)) {
15287 continue;
15288 }
15289 record_id = native_json_obj_get_long_default((yyjson_val *)row, identity_field, 0, NULL);
15290 field_value = native_json_obj_get((yyjson_val *)row, sort_field);
15291 if (!long_from_value_runtime(field_value, &sort_value)) {
15292 sort_value = record_id;
15293 }
15294 if (sort_value <= 0) {
15295 sort_value = record_id;
15296 }
15297 if (count == capacity) {
15298 capacity = capacity == 0 ? 8 : capacity * 2;
15299 next = (generated_surface_order_item_runtime *)realloc(items, capacity * sizeof(*items));
15300 if (next == NULL) {
15301 ok = 0;
15302 break;
15303 }
15304 items = next;
15305 }
15306 items[count].row = row;
15307 items[count].sort_key = sort_value;
15308 items[count].record_id = record_id;
15309 count++;
15310 }
15311 if (ok && count > 1) {
15312 qsort(items, count, sizeof(*items), compare_generated_surface_order_item_runtime);
15313 }
15314 if (ok) {
15315 for (idx = 0; idx < count; idx++) {
15316 if (!yyjson_mut_obj_put(items[idx].row, yyjson_mut_strcpy(mut_doc, sort_field), yyjson_mut_int(mut_doc, (long)idx + 1))) {
15317 ok = 0;
15318 break;
15319 }
15320 }
15321 }
15322 free(items);
15323 free(sort_field);
15324 free(collection_name);
15325 free(scope_field);
15326 free(identity_field);
15327 return ok;
15328}
15329
15330static int generated_surface_append_audit_event_mut_runtime(yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, yyjson_val *surface_root, const char *operation_name, yyjson_val *user_root, yyjson_val *context_root, yyjson_val *item_root, const char *created_at) {
15331 yyjson_mut_val *audit_log = NULL;
15332 yyjson_mut_val *event_obj = NULL;
15333 char *entity_id = native_json_obj_get_string_dup(surface_root, "entity_id");
15334 char *surface_id = native_json_obj_get_string_dup(surface_root, "surface_id");
15335 char *event_name = NULL;
15336 char *identity_field = native_json_obj_get_string_dup(surface_root, "identity_field");
15337 char *actor_id_text = user_root != NULL ? json_value_tostring_dup_runtime(native_json_obj_get(user_root, "id")) : NULL;
15338 char *tenant_id_text = context_root != NULL ? json_value_tostring_dup_runtime(native_json_obj_get(context_root, "tenant_id")) : NULL;
15339 yyjson_val *entity_record_id = NULL;
15340 yyjson_mut_val *actor_value = NULL;
15341 yyjson_mut_val *tenant_value = NULL;
15342 if (entity_id == NULL || entity_id[0] == '\0') {
15343 free(entity_id);
15344 entity_id = strdup("entity");
15345 }
15346 if (surface_id == NULL || surface_id[0] == '\0') {
15347 free(surface_id);
15348 surface_id = strdup("surface");
15349 }
15350 if (identity_field == NULL || identity_field[0] == '\0') {
15351 free(identity_field);
15352 identity_field = strdup("id");
15353 }
15354 {
15355 yyjson_val *audit_events = native_json_obj_get(surface_root, "audit_events");
15356 event_name = audit_events != NULL ? native_json_obj_get_string_dup(audit_events, operation_name) : NULL;
15357 }
15358 if (event_name == NULL || event_name[0] == '\0') {
15359 free(event_name);
15360 event_name = (char *)malloc(strlen(operation_name != NULL ? operation_name : "operation") + 19);
15361 if (event_name != NULL) {
15362 snprintf(event_name, strlen(operation_name != NULL ? operation_name : "operation") + 19, "generated_surface_%s", operation_name != NULL ? operation_name : "operation");
15363 }
15364 }
15365 audit_log = ensure_mut_array_field_runtime(mut_doc, mut_root, "audit_log");
15366 event_obj = yyjson_mut_obj(mut_doc);
15367 entity_record_id = item_root != NULL ? native_json_obj_get(item_root, identity_field) : NULL;
15368 if (actor_id_text != NULL && actor_id_text[0] != '\0' && !streq(actor_id_text, "0") && !streq(actor_id_text, "null") && digits_only_text_runtime(actor_id_text)) {
15369 actor_value = yyjson_mut_int(mut_doc, strtol(actor_id_text, NULL, 10));
15370 } else {
15371 actor_value = yyjson_mut_null(mut_doc);
15372 }
15373 if (tenant_id_text != NULL && tenant_id_text[0] != '\0' && !streq(tenant_id_text, "0") && !streq(tenant_id_text, "null") && digits_only_text_runtime(tenant_id_text)) {
15374 tenant_value = yyjson_mut_int(mut_doc, strtol(tenant_id_text, NULL, 10));
15375 } else {
15376 tenant_value = yyjson_mut_null(mut_doc);
15377 }
15378 if (audit_log == NULL || event_obj == NULL || event_name == NULL || !yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "event", event_name) || !yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "created_at", created_at != NULL ? created_at : "") || !yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "surface_id", surface_id) || !yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "entity_id", entity_id) || !yyjson_mut_obj_add_strcpy(mut_doc, event_obj, "operation", operation_name != NULL ? operation_name : "") || !yyjson_mut_obj_put(event_obj, yyjson_mut_strcpy(mut_doc, "actor_user_id"), actor_value) || !yyjson_mut_obj_put(event_obj, yyjson_mut_strcpy(mut_doc, "tenant_id"), tenant_value) || !yyjson_mut_obj_put(event_obj, yyjson_mut_strcpy(mut_doc, "entity_record_id"), entity_record_id != NULL ? yyjson_val_mut_copy(mut_doc, entity_record_id) : yyjson_mut_null(mut_doc)) || !yyjson_mut_arr_append(audit_log, event_obj)) {
15379 free(entity_id);
15380 free(surface_id);
15381 free(event_name);
15382 free(identity_field);
15383 free(actor_id_text);
15384 free(tenant_id_text);
15385 return 0;
15386 }
15387 free(entity_id);
15388 free(surface_id);
15389 free(event_name);
15390 free(identity_field);
15391 free(actor_id_text);
15392 free(tenant_id_text);
15393 return 1;
15394}
15395
15397 yyjson_arr_iter iter;
15398 yyjson_val *item = NULL;
15399 long numeric = 0;
15400 if (array == NULL || !yyjson_is_arr(array) || !yyjson_arr_iter_init(array, &iter)) {
15401 return 0;
15402 }
15403 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
15404 if (!long_from_value_runtime(item, &numeric)) {
15405 return 0;
15406 }
15407 }
15408 return 1;
15409}
15410
15411static int generated_surface_text_is_decimal_runtime(const char *text) {
15412 size_t index = 0;
15413 int dot_count = 0;
15414 int decimals = 0;
15415 if (text == NULL || text[0] == '\0') {
15416 return 0;
15417 }
15418 for (index = 0; text[index] != '\0'; index++) {
15419 if (text[index] == '.') {
15420 dot_count++;
15421 if (dot_count > 1 || index == 0 || text[index + 1] == '\0') {
15422 return 0;
15423 }
15424 continue;
15425 }
15426 if (!isdigit((unsigned char)text[index])) {
15427 return 0;
15428 }
15429 if (dot_count == 1) {
15430 decimals++;
15431 if (decimals > 2) {
15432 return 0;
15433 }
15434 }
15435 }
15436 return 1;
15437}
15438
15439static int generated_surface_text_is_email_runtime(const char *text) {
15440 const char *at = NULL;
15441 const char *dot = NULL;
15442 if (text == NULL || text[0] == '\0') {
15443 return 0;
15444 }
15445 at = strchr(text, '@');
15446 if (at == NULL || at == text || at[1] == '\0') {
15447 return 0;
15448 }
15449 dot = strrchr(at + 1, '.');
15450 if (dot == NULL || dot == at + 1 || dot[1] == '\0' || strlen(dot + 1) < 2) {
15451 return 0;
15452 }
15453 return 1;
15454}
15455
15456static int generated_surface_text_is_time_runtime(const char *text) {
15457 if (text == NULL || strlen(text) != 5 || text[2] != ':' || !isdigit((unsigned char)text[0]) || !isdigit((unsigned char)text[1]) || !isdigit((unsigned char)text[3]) || !isdigit((unsigned char)text[4])) {
15458 return 0;
15459 }
15460 {
15461 int hour = (text[0] - '0') * 10 + (text[1] - '0');
15462 int minute = (text[3] - '0') * 10 + (text[4] - '0');
15463 return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59;
15464 }
15465}
15466
15467static int generated_surface_text_is_date_runtime(const char *text) {
15468 size_t i = 0;
15469 if (text == NULL || strlen(text) != 10 || text[4] != '-' || text[7] != '-') {
15470 return 0;
15471 }
15472 for (i = 0; i < 10; i++) {
15473 if (i == 4 || i == 7) {
15474 continue;
15475 }
15476 if (!isdigit((unsigned char)text[i])) {
15477 return 0;
15478 }
15479 }
15480 return 1;
15481}
15482
15483static yyjson_mut_val *surface_build_sort_config_runtime(yyjson_mut_doc *doc, yyjson_val *surface_root, const char *raw_query) {
15484 char *sort_key = query_value_decoded_dup_runtime(raw_query, "sort");
15485 char *default_sort = NULL;
15486 yyjson_val *sorts = NULL;
15487 yyjson_arr_iter iter;
15488 yyjson_val *item = NULL;
15489 if (doc == NULL || surface_root == NULL || !yyjson_is_obj(surface_root)) {
15490 free(sort_key);
15491 return yyjson_mut_obj(doc);
15492 }
15493 if (sort_key == NULL || sort_key[0] == '\0') {
15494 free(sort_key);
15495 default_sort = native_json_obj_get_string_dup(surface_root, "default_sort");
15496 sort_key = default_sort;
15497 }
15498 if (sort_key == NULL || sort_key[0] == '\0') {
15499 free(sort_key);
15500 return yyjson_mut_obj(doc);
15501 }
15502 sorts = native_json_obj_get_array(surface_root, "sorts");
15503 if (sorts != NULL && yyjson_arr_iter_init(sorts, &iter)) {
15504 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
15505 if (yyjson_equals_str(native_json_obj_get(item, "key"), sort_key)) {
15506 yyjson_mut_val *obj = yyjson_val_mut_copy(doc, item);
15507 if (obj != NULL && yyjson_mut_is_obj(obj)) {
15508 yyjson_mut_obj_put(obj, yyjson_mut_strcpy(doc, "key"), yyjson_mut_strcpy(doc, sort_key));
15509 }
15510 free(sort_key);
15511 return obj != NULL ? obj : yyjson_mut_obj(doc);
15512 }
15513 }
15514 }
15515 free(sort_key);
15516 return yyjson_mut_obj(doc);
15517}
15518
15519static yyjson_mut_val *surface_build_active_filters_runtime(yyjson_mut_doc *doc, yyjson_val *surface_root, const char *raw_query) {
15520 yyjson_mut_val *array = yyjson_mut_arr(doc);
15521 yyjson_val *filters = native_json_obj_get_array(surface_root, "filters");
15522 yyjson_arr_iter iter;
15523 yyjson_val *item = NULL;
15524 if (doc == NULL || array == NULL || filters == NULL || !yyjson_arr_iter_init(filters, &iter)) {
15525 return array;
15526 }
15527 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
15528 char *query_name = native_json_obj_get_string_dup(item, "query");
15529 char *value = query_name != NULL ? query_value_decoded_dup_runtime(raw_query, query_name) : NULL;
15530 if ((value == NULL || value[0] == '\0') && native_json_obj_get(item, "default") != NULL) {
15531 free(value);
15532 value = json_value_tostring_dup_runtime(native_json_obj_get(item, "default"));
15533 }
15534 if (value != NULL && value[0] != '\0') {
15535 yyjson_mut_val *entry = yyjson_val_mut_copy(doc, item);
15536 if (entry != NULL && yyjson_mut_is_obj(entry)) {
15537 yyjson_mut_obj_put(entry, yyjson_mut_strcpy(doc, "value"), yyjson_mut_strcpy(doc, value));
15538 yyjson_mut_arr_append(array, entry);
15539 }
15540 }
15541 free(query_name);
15542 free(value);
15543 }
15544 return array;
15545}
15546
15548 const char *surface_json = json_option_value_runtime(argc, argv, "--surface-json");
15549 yyjson_doc *surface_doc = NULL;
15550 yyjson_val *surface_root = load_root_from_text_runtime(surface_json, &surface_doc);
15551 yyjson_val *admin = NULL;
15552 yyjson_val *list_columns = NULL;
15553 yyjson_val *response_fields = NULL;
15554 yyjson_mut_doc *out_doc = NULL;
15555 yyjson_mut_val *out_array = NULL;
15556 yyjson_arr_iter iter;
15557 yyjson_val *item = NULL;
15558 int code = 69;
15559 size_t emitted = 0;
15560 if (surface_root == NULL || !yyjson_is_obj(surface_root)) {
15561 native_json_doc_free(surface_doc);
15562 return 64;
15563 }
15564 admin = native_json_obj_get(surface_root, "admin");
15565 list_columns = native_json_obj_get(admin, "list_columns");
15566 if (list_columns != NULL && yyjson_is_arr(list_columns)) {
15567 code = emit_compact_json_value_runtime(list_columns);
15568 native_json_doc_free(surface_doc);
15569 return code;
15570 }
15571 out_doc = yyjson_mut_doc_new(NULL);
15572 out_array = out_doc != NULL ? yyjson_mut_arr(out_doc) : NULL;
15573 response_fields = native_json_obj_get_array(surface_root, "response_fields");
15574 if (out_doc == NULL || out_array == NULL) {
15575 yyjson_mut_doc_free(out_doc);
15576 native_json_doc_free(surface_doc);
15577 return 69;
15578 }
15579 yyjson_mut_doc_set_root(out_doc, out_array);
15580 if (response_fields != NULL && yyjson_arr_iter_init(response_fields, &iter)) {
15581 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
15582 const char *field_name = yyjson_is_str(item) ? yyjson_get_str(item) : NULL;
15583 yyjson_mut_val *entry = NULL;
15584 char *label = NULL;
15585 if (field_name == NULL || streq(field_name, "id") || streq(field_name, "business_id")) {
15586 continue;
15587 }
15588 if (emitted >= 4) {
15589 break;
15590 }
15592 entry = yyjson_mut_obj(out_doc);
15593 if (entry != NULL && label != NULL && yyjson_mut_obj_add_strcpy(out_doc, entry, "field", field_name) && yyjson_mut_obj_add_strcpy(out_doc, entry, "label", label)) {
15594 yyjson_mut_arr_append(out_array, entry);
15595 emitted++;
15596 }
15597 free(label);
15598 }
15599 }
15601 yyjson_mut_doc_free(out_doc);
15602 native_json_doc_free(surface_doc);
15603 return code;
15604}
15605
15606static int handle_generated_surface_params_json_command(int argc, char **argv) {
15607 const char *surface_json = json_option_value_runtime(argc, argv, "--surface-json");
15608 const char *context_json = json_option_value_runtime(argc, argv, "--context-json");
15609 const char *identity_value = json_option_value_runtime(argc, argv, "--identity-value");
15610 yyjson_doc *surface_doc = NULL;
15611 yyjson_doc *context_doc = NULL;
15612 yyjson_val *surface_root = load_root_from_text_runtime(surface_json, &surface_doc);
15613 yyjson_val *context_root = load_root_from_text_runtime(context_json, &context_doc);
15614 yyjson_val *binding = NULL;
15615 yyjson_mut_doc *out_doc = NULL;
15616 yyjson_mut_val *out_root = NULL;
15617 char *context_param = NULL;
15618 char *context_value = NULL;
15619 char *identity_param = NULL;
15620 int code = 69;
15621 if (surface_root == NULL || context_root == NULL || !yyjson_is_obj(surface_root) || !yyjson_is_obj(context_root)) {
15622 native_json_doc_free(surface_doc);
15623 native_json_doc_free(context_doc);
15624 return 64;
15625 }
15626 binding = native_json_obj_get(surface_root, "context_binding");
15627 context_param = native_json_obj_get_string_dup(binding, "param");
15628 context_value = json_value_tostring_dup_runtime(native_json_obj_get(context_root, "context_value"));
15629 identity_param = native_json_obj_get_string_dup(surface_root, "identity_param");
15630 out_doc = yyjson_mut_doc_new(NULL);
15631 out_root = out_doc != NULL ? yyjson_mut_obj(out_doc) : NULL;
15632 if (out_doc == NULL || out_root == NULL) {
15633 goto generated_surface_params_cleanup;
15634 }
15635 yyjson_mut_doc_set_root(out_doc, out_root);
15636 if (context_param != NULL && context_param[0] != '\0' && context_value != NULL && context_value[0] != '\0') {
15637 yyjson_mut_obj_add_strcpy(out_doc, out_root, context_param, context_value);
15638 }
15639 if (identity_param != NULL && identity_param[0] != '\0' && identity_value != NULL && identity_value[0] != '\0') {
15640 yyjson_mut_obj_add_strcpy(out_doc, out_root, identity_param, identity_value);
15641 }
15643 generated_surface_params_cleanup:
15644 free(context_param);
15645 free(context_value);
15646 free(identity_param);
15647 native_json_doc_free(surface_doc);
15648 native_json_doc_free(context_doc);
15649 yyjson_mut_doc_free(out_doc);
15650 return code;
15651}
15652
15653static char *generated_surface_display_value_dup_runtime(yyjson_val *item_root, const char *field_name) {
15654 yyjson_val *value = NULL;
15655 TextBufferRuntime buffer = {0};
15656 char *text = NULL;
15657 if (item_root == NULL || !yyjson_is_obj(item_root) || field_name == NULL || field_name[0] == '\0') {
15658 return NULL;
15659 }
15660 value = native_json_obj_get(item_root, field_name);
15661 if (value == NULL || yyjson_is_null(value)) {
15662 return strdup("");
15663 }
15664 if (yyjson_is_arr(value)) {
15665 yyjson_arr_iter iter;
15666 yyjson_val *entry = NULL;
15667 int first = 1;
15668 if (yyjson_arr_iter_init(value, &iter)) {
15669 while ((entry = yyjson_arr_iter_next(&iter)) != NULL) {
15670 text = yyjson_is_bool(entry) ? strdup(yyjson_get_bool(entry) ? "yes" : "no") : json_value_tostring_dup_runtime(entry);
15671 if (text != NULL) {
15672 if (!first && !text_buffer_append_text_runtime(&buffer, ", ")) {
15673 free(text);
15674 text_buffer_free_runtime(&buffer);
15675 return NULL;
15676 }
15677 if (!text_buffer_append_text_runtime(&buffer, text)) {
15678 free(text);
15679 text_buffer_free_runtime(&buffer);
15680 return NULL;
15681 }
15682 first = 0;
15683 free(text);
15684 text = NULL;
15685 }
15686 }
15687 }
15688 return text_buffer_take_runtime(&buffer);
15689 }
15690 return yyjson_is_bool(value) ? strdup(yyjson_get_bool(value) ? "yes" : "no") : json_value_tostring_dup_runtime(value);
15691}
15692
15693static int handle_generated_surface_display_value_command(int argc, char **argv) {
15694 const char *item_json = json_option_value_runtime(argc, argv, "--item-json");
15695 const char *field_name = json_option_value_runtime(argc, argv, "--field-name");
15696 yyjson_doc *item_doc = NULL;
15697 yyjson_val *item_root = load_root_from_text_runtime(item_json, &item_doc);
15698 char *text = NULL;
15699 int code = 0;
15700 if (item_root == NULL || !yyjson_is_obj(item_root) || field_name == NULL || field_name[0] == '\0') {
15701 native_json_doc_free(item_doc);
15702 return 64;
15703 }
15704 text = generated_surface_display_value_dup_runtime(item_root, field_name);
15705 if (text != NULL) {
15706 fputs(text, stdout);
15707 free(text);
15708 }
15709 native_json_doc_free(item_doc);
15710 return code;
15711}
15712
15713static char *html_escape_dup_runtime(const char *text) {
15714 size_t index = 0;
15715 size_t needed = 1;
15716 char *out = NULL;
15717 size_t cursor = 0;
15718 if (text == NULL) {
15719 return strdup("");
15720 }
15721 for (index = 0; text[index] != '\0'; index++) {
15722 switch (text[index]) {
15723 case '&': needed += 5; break;
15724 case '<':
15725 case '>': needed += 4; break;
15726 case '"': needed += 6; break;
15727 case '\'': needed += 5; break;
15728 default: needed += 1; break;
15729 }
15730 }
15731 out = (char *)malloc(needed);
15732 if (out == NULL) {
15733 return NULL;
15734 }
15735 for (index = 0; text[index] != '\0'; index++) {
15736 const char *replacement = NULL;
15737 switch (text[index]) {
15738 case '&': replacement = "&amp;"; break;
15739 case '<': replacement = "&lt;"; break;
15740 case '>': replacement = "&gt;"; break;
15741 case '"': replacement = "&quot;"; break;
15742 case '\'': replacement = "&#39;"; break;
15743 default: replacement = NULL; break;
15744 }
15745 if (replacement != NULL) {
15746 size_t repl_len = strlen(replacement);
15747 memcpy(out + cursor, replacement, repl_len);
15748 cursor += repl_len;
15749 } else {
15750 out[cursor++] = text[index];
15751 }
15752 }
15753 out[cursor] = '\0';
15754 return out;
15755}
15756
15757static int text_buffer_append_html_escaped_runtime(TextBufferRuntime *buffer, const char *text) {
15758 char *escaped = html_escape_dup_runtime(text != NULL ? text : "");
15759 int ok = escaped != NULL && text_buffer_append_text_runtime(buffer, escaped);
15760 free(escaped);
15761 return ok;
15762}
15763
15764static int append_generated_surface_select_options_html_runtime(TextBufferRuntime *buffer, yyjson_val *field_root, const char *selected_value) {
15765 yyjson_val *options = NULL;
15766 yyjson_arr_iter iter;
15767 yyjson_val *option = NULL;
15768 int selected_found = 0;
15769 if (buffer == NULL || field_root == NULL || !yyjson_is_obj(field_root)) {
15770 return 0;
15771 }
15772 options = native_json_obj_get_array(field_root, "options");
15773 if (options != NULL && yyjson_arr_iter_init(options, &iter)) {
15774 while ((option = yyjson_arr_iter_next(&iter)) != NULL) {
15775 char *option_value = native_json_obj_get_string_dup(option, "value");
15776 char *option_label = native_json_obj_get_string_dup(option, "label");
15777 int is_selected = 0;
15778 if (option_value == NULL || option_value[0] == '\0') {
15779 free(option_value);
15780 option_value = json_value_tostring_dup_runtime(native_json_obj_get(option, "id"));
15781 }
15782 if (option_value == NULL || option_value[0] == '\0') {
15783 free(option_value);
15784 option_value = native_json_obj_get_string_dup(option, "name");
15785 }
15786 if (option_label == NULL || option_label[0] == '\0') {
15787 free(option_label);
15788 option_label = native_json_obj_get_string_dup(option, "name");
15789 }
15790 if ((option_label == NULL || option_label[0] == '\0') && option_value != NULL) {
15791 free(option_label);
15792 option_label = strdup(option_value);
15793 }
15794 is_selected = option_value != NULL && selected_value != NULL && streq(option_value, selected_value);
15795 if (option_value != NULL && option_label != NULL) {
15796 if (!text_buffer_append_text_runtime(buffer, "<option value=\"") ||
15797 !text_buffer_append_html_escaped_runtime(buffer, option_value) ||
15798 !text_buffer_append_text_runtime(buffer, is_selected ? "\" selected>" : "\">") ||
15799 !text_buffer_append_html_escaped_runtime(buffer, option_label) ||
15800 !text_buffer_append_text_runtime(buffer, "</option>")) {
15801 free(option_value);
15802 free(option_label);
15803 return 0;
15804 }
15805 if (is_selected) {
15806 selected_found = 1;
15807 }
15808 }
15809 free(option_value);
15810 free(option_label);
15811 }
15812 }
15813 if (!selected_found && selected_value != NULL && selected_value[0] != '\0') {
15814 if (!text_buffer_append_text_runtime(buffer, "<option value=\"") ||
15815 !text_buffer_append_html_escaped_runtime(buffer, selected_value) ||
15816 !text_buffer_append_text_runtime(buffer, "\" selected>") ||
15817 !text_buffer_append_html_escaped_runtime(buffer, selected_value) ||
15818 !text_buffer_append_text_runtime(buffer, "</option>")) {
15819 return 0;
15820 }
15821 }
15822 return 1;
15823}
15824
15825static int append_generated_surface_relation_checkboxes_html_runtime(TextBufferRuntime *buffer, yyjson_val *state_root, yyjson_val *relation_root, yyjson_val *context_root, yyjson_val *selected_root) {
15826 yyjson_val *rows = NULL;
15827 yyjson_val *context_id = NULL;
15828 yyjson_arr_iter iter;
15829 yyjson_val *row = NULL;
15830 char *relation_field = NULL;
15831 char *related_collection = NULL;
15832 char *related_scope_field = NULL;
15833 char *label_field = NULL;
15834 char *checkbox_prefix = NULL;
15835 char *relation_label = NULL;
15836 char *relation_help_text = NULL;
15837 char *empty_options_text = NULL;
15838 int emitted = 0;
15839 int ok = 1;
15840 if (buffer == NULL || state_root == NULL || relation_root == NULL || context_root == NULL || !yyjson_is_obj(state_root) || !yyjson_is_obj(relation_root) || !yyjson_is_obj(context_root)) {
15841 return 0;
15842 }
15843 relation_field = native_json_obj_get_string_dup(relation_root, "field");
15844 related_collection = native_json_obj_get_string_dup(relation_root, "related_collection");
15845 related_scope_field = native_json_obj_get_string_dup(relation_root, "related_scope_field");
15846 label_field = native_json_obj_get_string_dup(relation_root, "label_field");
15847 checkbox_prefix = native_json_obj_get_string_dup(relation_root, "form_checkbox_prefix");
15848 relation_label = native_json_obj_get_string_dup(relation_root, "label");
15849 relation_help_text = native_json_obj_get_string_dup(relation_root, "help_text");
15850 empty_options_text = native_json_obj_get_string_dup(relation_root, "empty_options_text");
15851 if (label_field == NULL || label_field[0] == '\0') {
15852 free(label_field);
15853 label_field = strdup("name");
15854 }
15855 if (checkbox_prefix == NULL || checkbox_prefix[0] == '\0') {
15856 size_t needed = strlen(relation_field != NULL ? relation_field : "field") + 3;
15857 free(checkbox_prefix);
15858 checkbox_prefix = (char *)malloc(needed);
15859 if (checkbox_prefix != NULL) {
15860 snprintf(checkbox_prefix, needed, "%s__", relation_field != NULL ? relation_field : "field");
15861 }
15862 }
15863 if (relation_label == NULL || relation_label[0] == '\0') {
15864 free(relation_label);
15865 relation_label = strdup(relation_field != NULL ? relation_field : "field");
15866 }
15867 if (empty_options_text == NULL || empty_options_text[0] == '\0') {
15868 free(empty_options_text);
15869 empty_options_text = strdup("No related records available.");
15870 }
15871 rows = related_collection != NULL ? state_array_runtime(state_root, related_collection) : NULL;
15872 context_id = native_json_obj_get(context_root, "context_id");
15873 ok = text_buffer_append_text_runtime(buffer, "<div class=\"form-group\">");
15874 if (ok && relation_label != NULL) {
15875 ok = text_buffer_append_text_runtime(buffer, "<label>") && text_buffer_append_html_escaped_runtime(buffer, relation_label) && text_buffer_append_text_runtime(buffer, "</label>");
15876 }
15877 if (ok && relation_help_text != NULL && relation_help_text[0] != '\0') {
15878 ok = text_buffer_append_text_runtime(buffer, "<p class=\"muted\">") && text_buffer_append_html_escaped_runtime(buffer, relation_help_text) && text_buffer_append_text_runtime(buffer, "</p>");
15879 }
15880 if (ok) {
15881 ok = text_buffer_append_text_runtime(buffer, "<div class=\"stack\">");
15882 }
15883 if (ok && rows != NULL && yyjson_is_arr(rows) && yyjson_arr_iter_init(rows, &iter)) {
15884 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
15885 long row_id = 0;
15886 char *row_label = NULL;
15887 if (!yyjson_is_obj(row) || !surface_context_match_runtime(row, related_scope_field, context_id) || !long_from_value_runtime(native_json_obj_get(row, "id"), &row_id)) {
15888 continue;
15889 }
15890 row_label = native_json_obj_get_string_dup(row, label_field);
15891 if (row_label == NULL || row_label[0] == '\0') {
15892 free(row_label);
15894 }
15895 ok = text_buffer_append_text_runtime(buffer, "<label><input type=\"checkbox\" name=\"") &&
15896 text_buffer_append_html_escaped_runtime(buffer, checkbox_prefix != NULL ? checkbox_prefix : "field__") &&
15897 text_buffer_append_format_runtime(buffer, "%ld\" value=\"%ld\"", row_id, row_id) &&
15898 text_buffer_append_text_runtime(buffer, json_array_contains_long_runtime(selected_root, row_id) ? " checked> " : "> ") &&
15899 text_buffer_append_html_escaped_runtime(buffer, row_label != NULL ? row_label : "") &&
15900 text_buffer_append_text_runtime(buffer, "</label>");
15901 free(row_label);
15902 if (!ok) {
15903 break;
15904 }
15905 emitted = 1;
15906 }
15907 }
15908 if (ok && !emitted && empty_options_text != NULL) {
15909 ok = text_buffer_append_text_runtime(buffer, "<p class=\"muted\">") && text_buffer_append_html_escaped_runtime(buffer, empty_options_text) && text_buffer_append_text_runtime(buffer, "</p>");
15910 }
15911 if (ok) {
15912 ok = text_buffer_append_text_runtime(buffer, "</div></div>");
15913 }
15914 free(relation_field);
15915 free(related_collection);
15916 free(related_scope_field);
15917 free(label_field);
15918 free(checkbox_prefix);
15919 free(relation_label);
15920 free(relation_help_text);
15921 free(empty_options_text);
15922 return ok;
15923}
15924
15925static char *build_generated_surface_form_fields_html_runtime(yyjson_val *state_root, yyjson_val *fixture_root, yyjson_val *surface_root, yyjson_val *context_root, yyjson_val *item_root) {
15926 TextBufferRuntime buffer = {0};
15927 yyjson_val *form = NULL;
15928 yyjson_val *fields = NULL;
15929 yyjson_val *relation_inputs = NULL;
15930 yyjson_arr_iter iter;
15931 yyjson_val *field = NULL;
15932 char *form_id = NULL;
15933 if (fixture_root == NULL || surface_root == NULL || !yyjson_is_obj(fixture_root) || !yyjson_is_obj(surface_root)) {
15934 return NULL;
15935 }
15936 form_id = native_json_obj_get_string_dup(surface_root, "form_id");
15937 form = surface_find_form_runtime(fixture_root, form_id);
15938 free(form_id);
15939 fields = form != NULL ? native_json_obj_get_array(form, "fields") : NULL;
15940 if (fields != NULL && yyjson_arr_iter_init(fields, &iter)) {
15941 while ((field = yyjson_arr_iter_next(&iter)) != NULL) {
15942 char *field_name = native_json_obj_get_string_dup(field, "field_id");
15943 char *widget = native_json_obj_get_string_dup(field, "widget");
15944 char *field_label = native_json_obj_get_string_dup(field, "label");
15945 char *field_help_text = native_json_obj_get_string_dup(field, "help_text");
15946 char *field_placeholder = native_json_obj_get_string_dup(field, "placeholder");
15947 char *value = NULL;
15948 TextBufferRuntime options = {0};
15949 int required = json_value_truthy_runtime(native_json_obj_get(field, "required"));
15950 int ok = 1;
15951 if (field_name == NULL || field_name[0] == '\0') {
15952 free(field_name);
15953 free(widget);
15954 free(field_label);
15955 free(field_help_text);
15956 free(field_placeholder);
15957 text_buffer_free_runtime(&options);
15958 continue;
15959 }
15960 if (widget == NULL || widget[0] == '\0') {
15961 free(widget);
15962 widget = strdup("text");
15963 }
15964 if (field_label == NULL || field_label[0] == '\0') {
15965 free(field_label);
15966 field_label = generated_surface_label_from_field_runtime(field_name);
15967 }
15968 value = item_root != NULL && yyjson_is_obj(item_root) ? json_value_tostring_dup_runtime(native_json_obj_get(item_root, field_name)) : strdup("");
15969 if (widget != NULL && streq(widget, "select") && !append_generated_surface_select_options_html_runtime(&options, field, value != NULL ? value : "")) {
15970 ok = 0;
15971 }
15972 if (!ok) {
15973 free(field_name);
15974 free(widget);
15975 free(field_label);
15976 free(field_help_text);
15977 free(field_placeholder);
15978 free(value);
15979 text_buffer_free_runtime(&options);
15980 text_buffer_free_runtime(&buffer);
15981 return NULL;
15982 }
15983 if (widget != NULL && streq(widget, "textarea")) {
15984 ok = text_buffer_append_text_runtime(&buffer, "<label>") &&
15985 text_buffer_append_html_escaped_runtime(&buffer, field_label != NULL ? field_label : field_name) &&
15986 text_buffer_append_text_runtime(&buffer, "<textarea name=\"") &&
15987 text_buffer_append_html_escaped_runtime(&buffer, field_name) &&
15988 text_buffer_append_text_runtime(&buffer, "\" class=\"form-control\"") &&
15989 (!required || text_buffer_append_text_runtime(&buffer, " required")) &&
15990 (field_placeholder == NULL || field_placeholder[0] == '\0' || (text_buffer_append_text_runtime(&buffer, " placeholder=\"") && text_buffer_append_html_escaped_runtime(&buffer, field_placeholder) && text_buffer_append_text_runtime(&buffer, "\""))) &&
15991 text_buffer_append_text_runtime(&buffer, ">") &&
15992 text_buffer_append_html_escaped_runtime(&buffer, value != NULL ? value : "") &&
15993 text_buffer_append_text_runtime(&buffer, "</textarea></label>");
15994 } else if (widget != NULL && streq(widget, "select") && options.length > 0) {
15995 ok = text_buffer_append_text_runtime(&buffer, "<label>") &&
15996 text_buffer_append_html_escaped_runtime(&buffer, field_label != NULL ? field_label : field_name) &&
15997 text_buffer_append_text_runtime(&buffer, "<select name=\"") &&
15998 text_buffer_append_html_escaped_runtime(&buffer, field_name) &&
15999 text_buffer_append_text_runtime(&buffer, "\" class=\"form-control\"") &&
16000 (!required || text_buffer_append_text_runtime(&buffer, " required")) &&
16001 text_buffer_append_text_runtime(&buffer, ">") &&
16002 text_buffer_append_text_runtime(&buffer, options.text != NULL ? options.text : "") &&
16003 text_buffer_append_text_runtime(&buffer, "</select></label>");
16004 } else {
16005 const char *input_type = "text";
16006 if (widget != NULL && (streq(widget, "number") || streq(widget, "email") || streq(widget, "time") || streq(widget, "date") || streq(widget, "datetime-local") || streq(widget, "password"))) {
16007 input_type = widget;
16008 }
16009 ok = text_buffer_append_text_runtime(&buffer, "<label>") &&
16010 text_buffer_append_html_escaped_runtime(&buffer, field_label != NULL ? field_label : field_name) &&
16011 text_buffer_append_text_runtime(&buffer, "<input type=\"") &&
16012 text_buffer_append_html_escaped_runtime(&buffer, input_type) &&
16013 text_buffer_append_text_runtime(&buffer, "\" name=\"") &&
16014 text_buffer_append_html_escaped_runtime(&buffer, field_name) &&
16015 text_buffer_append_text_runtime(&buffer, "\" class=\"form-control\" value=\"") &&
16016 text_buffer_append_html_escaped_runtime(&buffer, value != NULL ? value : "") &&
16017 text_buffer_append_text_runtime(&buffer, "\"") &&
16018 (!required || text_buffer_append_text_runtime(&buffer, " required")) &&
16019 (field_placeholder == NULL || field_placeholder[0] == '\0' || (text_buffer_append_text_runtime(&buffer, " placeholder=\"") && text_buffer_append_html_escaped_runtime(&buffer, field_placeholder) && text_buffer_append_text_runtime(&buffer, "\""))) &&
16020 text_buffer_append_text_runtime(&buffer, "></label>");
16021 }
16022 if (ok && field_help_text != NULL && field_help_text[0] != '\0') {
16023 ok = text_buffer_append_text_runtime(&buffer, "<p class=\"muted\">") && text_buffer_append_html_escaped_runtime(&buffer, field_help_text) && text_buffer_append_text_runtime(&buffer, "</p>");
16024 }
16025 free(field_name);
16026 free(widget);
16027 free(field_label);
16028 free(field_help_text);
16029 free(field_placeholder);
16030 free(value);
16031 text_buffer_free_runtime(&options);
16032 if (!ok) {
16033 text_buffer_free_runtime(&buffer);
16034 return NULL;
16035 }
16036 }
16037 }
16038 relation_inputs = native_json_obj_get_array(surface_root, "relation_inputs");
16039 if (state_root != NULL && relation_inputs != NULL && yyjson_arr_iter_init(relation_inputs, &iter)) {
16040 while ((field = yyjson_arr_iter_next(&iter)) != NULL) {
16041 char *relation_field = native_json_obj_get_string_dup(field, "field");
16042 yyjson_val *selected_root = relation_field != NULL && item_root != NULL && yyjson_is_obj(item_root) ? native_json_obj_get(item_root, relation_field) : NULL;
16043 int ok = append_generated_surface_relation_checkboxes_html_runtime(&buffer, state_root, field, context_root, selected_root);
16044 free(relation_field);
16045 if (!ok) {
16046 text_buffer_free_runtime(&buffer);
16047 return NULL;
16048 }
16049 }
16050 }
16051 return text_buffer_take_runtime(&buffer);
16052}
16053
16054static char *build_generated_surface_filter_controls_html_runtime(yyjson_val *surface_root, const char *raw_query) {
16055 TextBufferRuntime buffer = {0};
16056 yyjson_val *filters = native_json_obj_get_array(surface_root, "filters");
16057 yyjson_arr_iter iter;
16058 yyjson_val *filter = NULL;
16059 if (surface_root == NULL || !yyjson_is_obj(surface_root) || filters == NULL || !yyjson_arr_iter_init(filters, &iter)) {
16060 return strdup("");
16061 }
16062 while ((filter = yyjson_arr_iter_next(&iter)) != NULL) {
16063 char *query_name = native_json_obj_get_string_dup(filter, "query");
16064 char *filter_label = native_json_obj_get_string_dup(filter, "label");
16065 char *field_name = native_json_obj_get_string_dup(filter, "field");
16066 char *match_kind = native_json_obj_get_string_dup(filter, "match");
16067 char *filter_value = query_name != NULL ? query_value_decoded_dup_runtime(raw_query != NULL ? raw_query : "", query_name) : NULL;
16068 char *default_value = json_value_tostring_dup_runtime(native_json_obj_get(filter, "default"));
16069 TextBufferRuntime options = {0};
16070 int ok = 1;
16071 if ((filter_label == NULL || filter_label[0] == '\0') && field_name != NULL && field_name[0] != '\0') {
16072 free(filter_label);
16073 filter_label = strdup(field_name);
16074 }
16075 if ((filter_label == NULL || filter_label[0] == '\0') && query_name != NULL && query_name[0] != '\0') {
16076 free(filter_label);
16077 filter_label = strdup(query_name);
16078 }
16079 if (match_kind == NULL || match_kind[0] == '\0') {
16080 free(match_kind);
16081 match_kind = strdup("exact");
16082 }
16083 if ((filter_value == NULL || filter_value[0] == '\0') && default_value != NULL && default_value[0] != '\0') {
16084 free(filter_value);
16085 filter_value = strdup(default_value);
16086 }
16087 if (match_kind != NULL && streq(match_kind, "boolean")) {
16088 const char *selected_true = "";
16089 const char *selected_false = "";
16090 const char *selected_all = "";
16091 if (filter_value != NULL && streq(filter_value, "false")) {
16092 selected_false = " selected";
16093 } else if (filter_value != NULL && streq(filter_value, "all")) {
16094 selected_all = " selected";
16095 } else {
16096 selected_true = " selected";
16097 }
16098 ok = text_buffer_append_format_runtime(&options, "<option value=\"true\"%s>Active only</option><option value=\"false\"%s>Inactive only</option><option value=\"all\"%s>All</option>", selected_true, selected_false, selected_all) &&
16099 text_buffer_append_text_runtime(&buffer, "<label>") && text_buffer_append_html_escaped_runtime(&buffer, filter_label != NULL ? filter_label : "filter") && text_buffer_append_text_runtime(&buffer, "<select name=\"") && text_buffer_append_html_escaped_runtime(&buffer, query_name != NULL ? query_name : "") && text_buffer_append_text_runtime(&buffer, "\" class=\"form-control\">") && text_buffer_append_text_runtime(&buffer, options.text != NULL ? options.text : "") && text_buffer_append_text_runtime(&buffer, "</select></label>");
16100 } else {
16101 if (append_generated_surface_select_options_html_runtime(&options, filter, filter_value != NULL ? filter_value : "") && options.length > 0) {
16102 ok = text_buffer_append_text_runtime(&buffer, "<label>") && text_buffer_append_html_escaped_runtime(&buffer, filter_label != NULL ? filter_label : "filter") && text_buffer_append_text_runtime(&buffer, "<select name=\"") && text_buffer_append_html_escaped_runtime(&buffer, query_name != NULL ? query_name : "") && text_buffer_append_text_runtime(&buffer, "\" class=\"form-control\"><option value=\"\">All</option>") && text_buffer_append_text_runtime(&buffer, options.text != NULL ? options.text : "") && text_buffer_append_text_runtime(&buffer, "</select></label>");
16103 } else {
16104 const char *input_type = match_kind != NULL && streq(match_kind, "array_contains_integer") ? "number" : "text";
16105 ok = text_buffer_append_text_runtime(&buffer, "<label>") && text_buffer_append_html_escaped_runtime(&buffer, filter_label != NULL ? filter_label : "filter") && text_buffer_append_text_runtime(&buffer, "<input type=\"") && text_buffer_append_text_runtime(&buffer, input_type) && text_buffer_append_text_runtime(&buffer, "\" name=\"") && text_buffer_append_html_escaped_runtime(&buffer, query_name != NULL ? query_name : "") && text_buffer_append_text_runtime(&buffer, "\" class=\"form-control\" value=\"") && text_buffer_append_html_escaped_runtime(&buffer, filter_value != NULL ? filter_value : "") && text_buffer_append_text_runtime(&buffer, "\"></label>");
16106 }
16107 }
16108 free(query_name);
16109 free(filter_label);
16110 free(field_name);
16111 free(match_kind);
16112 free(filter_value);
16113 free(default_value);
16114 text_buffer_free_runtime(&options);
16115 if (!ok) {
16116 text_buffer_free_runtime(&buffer);
16117 return NULL;
16118 }
16119 }
16120 return text_buffer_take_runtime(&buffer);
16121}
16122
16123static char *build_generated_surface_sort_options_html_runtime(yyjson_val *surface_root, const char *raw_query) {
16124 TextBufferRuntime buffer = {0};
16125 yyjson_val *sorts = native_json_obj_get_array(surface_root, "sorts");
16126 yyjson_arr_iter iter;
16127 yyjson_val *sort = NULL;
16128 char *current_sort = query_value_decoded_dup_runtime(raw_query != NULL ? raw_query : "", "sort");
16129 if ((current_sort == NULL || current_sort[0] == '\0') && surface_root != NULL && yyjson_is_obj(surface_root)) {
16130 free(current_sort);
16131 current_sort = native_json_obj_get_string_dup(surface_root, "default_sort");
16132 }
16133 if (surface_root == NULL || !yyjson_is_obj(surface_root) || sorts == NULL || !yyjson_arr_iter_init(sorts, &iter)) {
16134 free(current_sort);
16135 return strdup("");
16136 }
16137 while ((sort = yyjson_arr_iter_next(&iter)) != NULL) {
16138 char *sort_key = native_json_obj_get_string_dup(sort, "key");
16139 char *sort_label = native_json_obj_get_string_dup(sort, "label");
16140 int ok = 1;
16141 if ((sort_label == NULL || sort_label[0] == '\0') && sort_key != NULL) {
16142 free(sort_label);
16143 sort_label = strdup(sort_key);
16144 }
16145 if (sort_key != NULL && sort_key[0] != '\0') {
16146 ok = text_buffer_append_text_runtime(&buffer, "<option value=\"") &&
16147 text_buffer_append_html_escaped_runtime(&buffer, sort_key) &&
16148 text_buffer_append_text_runtime(&buffer, current_sort != NULL && streq(current_sort, sort_key) ? "\" selected>" : "\">") &&
16149 text_buffer_append_html_escaped_runtime(&buffer, sort_label != NULL ? sort_label : sort_key) &&
16150 text_buffer_append_text_runtime(&buffer, "</option>");
16151 }
16152 free(sort_key);
16153 free(sort_label);
16154 if (!ok) {
16155 free(current_sort);
16156 text_buffer_free_runtime(&buffer);
16157 return NULL;
16158 }
16159 }
16160 free(current_sort);
16161 return text_buffer_take_runtime(&buffer);
16162}
16163
16165 const char *field_json = json_option_value_runtime(argc, argv, "--field-json");
16166 const char *selected_value = json_option_value_runtime(argc, argv, "--selected-value");
16167 yyjson_doc *field_doc = NULL;
16168 yyjson_val *field_root = load_root_from_text_runtime(field_json, &field_doc);
16169 yyjson_val *options = NULL;
16170 yyjson_arr_iter iter;
16171 yyjson_val *option = NULL;
16172 int selected_found = 0;
16173 if (field_root == NULL || !yyjson_is_obj(field_root)) {
16174 native_json_doc_free(field_doc);
16175 return 64;
16176 }
16177 options = native_json_obj_get_array(field_root, "options");
16178 if (options != NULL && yyjson_arr_iter_init(options, &iter)) {
16179 while ((option = yyjson_arr_iter_next(&iter)) != NULL) {
16180 char *option_value = native_json_obj_get_string_dup(option, "value");
16181 char *option_label = native_json_obj_get_string_dup(option, "label");
16182 if ((option_value == NULL || option_value[0] == '\0')) {
16183 free(option_value);
16184 option_value = json_value_tostring_dup_runtime(native_json_obj_get(option, "id"));
16185 }
16186 if ((option_value == NULL || option_value[0] == '\0')) {
16187 free(option_value);
16188 option_value = native_json_obj_get_string_dup(option, "name");
16189 }
16190 if ((option_label == NULL || option_label[0] == '\0')) {
16191 free(option_label);
16192 option_label = native_json_obj_get_string_dup(option, "name");
16193 }
16194 if ((option_label == NULL || option_label[0] == '\0') && option_value != NULL) {
16195 option_label = strdup(option_value);
16196 }
16197 if (option_value != NULL && option_label != NULL) {
16198 int is_selected = selected_value != NULL && streq(option_value, selected_value);
16199 char *escaped_value = html_escape_dup_runtime(option_value);
16200 char *escaped_label = html_escape_dup_runtime(option_label);
16201 if (escaped_value != NULL && escaped_label != NULL) {
16202 fprintf(stdout, "<option value=\"%s\"%s>%s</option>", escaped_value, is_selected ? " selected" : "", escaped_label);
16203 if (is_selected) {
16204 selected_found = 1;
16205 }
16206 }
16207 free(escaped_value);
16208 free(escaped_label);
16209 }
16210 free(option_value);
16211 free(option_label);
16212 }
16213 }
16214 if (!selected_found && selected_value != NULL && selected_value[0] != '\0') {
16215 char *escaped_value = html_escape_dup_runtime(selected_value);
16216 if (escaped_value != NULL) {
16217 fprintf(stdout, "<option value=\"%s\" selected>%s</option>", escaped_value, escaped_value);
16218 free(escaped_value);
16219 }
16220 }
16221 native_json_doc_free(field_doc);
16222 return 0;
16223}
16224
16226 const char *state_path = json_option_value_runtime(argc, argv, "--state");
16227 const char *relation_json = json_option_value_runtime(argc, argv, "--relation-json");
16228 const char *context_json = json_option_value_runtime(argc, argv, "--context-json");
16229 const char *selected_json = json_option_value_runtime(argc, argv, "--selected-json");
16230 yyjson_doc *state_doc = NULL;
16231 yyjson_doc *relation_doc = NULL;
16232 yyjson_doc *context_doc = NULL;
16233 yyjson_doc *selected_doc = NULL;
16234 yyjson_val *state_root = NULL;
16235 yyjson_val *relation_root = load_root_from_text_runtime(relation_json, &relation_doc);
16236 yyjson_val *context_root = load_root_from_text_runtime(context_json, &context_doc);
16237 yyjson_val *selected_root = load_root_from_text_runtime(selected_json != NULL ? selected_json : "[]", &selected_doc);
16238 yyjson_val *rows = NULL;
16239 yyjson_val *context_id = NULL;
16240 yyjson_arr_iter iter;
16241 yyjson_val *row = NULL;
16242 char *relation_field = NULL;
16243 char *related_collection = NULL;
16244 char *related_scope_field = NULL;
16245 char *label_field = NULL;
16246 char *checkbox_prefix = NULL;
16247 char *relation_label = NULL;
16248 char *relation_help_text = NULL;
16249 char *empty_options_text = NULL;
16250 int emitted = 0;
16251 if (state_path == NULL || relation_root == NULL || context_root == NULL || !yyjson_is_obj(relation_root) || !yyjson_is_obj(context_root)) {
16252 native_json_doc_free(relation_doc);
16253 native_json_doc_free(context_doc);
16254 native_json_doc_free(selected_doc);
16255 return 64;
16256 }
16257 state_doc = native_json_doc_load_file(state_path);
16258 state_root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
16259 if (state_root == NULL || !yyjson_is_obj(state_root)) {
16260 native_json_doc_free(state_doc);
16261 native_json_doc_free(relation_doc);
16262 native_json_doc_free(context_doc);
16263 native_json_doc_free(selected_doc);
16264 return 64;
16265 }
16266 relation_field = native_json_obj_get_string_dup(relation_root, "field");
16267 related_collection = native_json_obj_get_string_dup(relation_root, "related_collection");
16268 related_scope_field = native_json_obj_get_string_dup(relation_root, "related_scope_field");
16269 label_field = native_json_obj_get_string_dup(relation_root, "label_field");
16270 checkbox_prefix = native_json_obj_get_string_dup(relation_root, "form_checkbox_prefix");
16271 relation_label = native_json_obj_get_string_dup(relation_root, "label");
16272 relation_help_text = native_json_obj_get_string_dup(relation_root, "help_text");
16273 empty_options_text = native_json_obj_get_string_dup(relation_root, "empty_options_text");
16274 if (label_field == NULL || label_field[0] == '\0') {
16275 free(label_field);
16276 label_field = strdup("name");
16277 }
16278 if (checkbox_prefix == NULL || checkbox_prefix[0] == '\0') {
16279 size_t needed = strlen(relation_field != NULL ? relation_field : "field") + 3;
16280 free(checkbox_prefix);
16281 checkbox_prefix = (char *)malloc(needed);
16282 if (checkbox_prefix != NULL) {
16283 snprintf(checkbox_prefix, needed, "%s__", relation_field != NULL ? relation_field : "field");
16284 }
16285 }
16286 if (relation_label == NULL || relation_label[0] == '\0') {
16287 free(relation_label);
16288 relation_label = strdup(relation_field != NULL ? relation_field : "field");
16289 }
16290 if (empty_options_text == NULL || empty_options_text[0] == '\0') {
16291 free(empty_options_text);
16292 empty_options_text = strdup("No related records available.");
16293 }
16294 rows = related_collection != NULL ? state_array_runtime(state_root, related_collection) : NULL;
16295 context_id = native_json_obj_get(context_root, "context_id");
16296 if (relation_label != NULL) {
16297 char *escaped = html_escape_dup_runtime(relation_label);
16298 if (escaped != NULL) {
16299 fprintf(stdout, "<div class=\"form-group\"><label>%s</label>", escaped);
16300 free(escaped);
16301 }
16302 }
16303 if (relation_help_text != NULL && relation_help_text[0] != '\0') {
16304 char *escaped = html_escape_dup_runtime(relation_help_text);
16305 if (escaped != NULL) {
16306 fprintf(stdout, "<p class=\"muted\">%s</p>", escaped);
16307 free(escaped);
16308 }
16309 }
16310 fprintf(stdout, "<div class=\"stack\">");
16311 if (rows != NULL && yyjson_is_arr(rows) && yyjson_arr_iter_init(rows, &iter)) {
16312 while ((row = yyjson_arr_iter_next(&iter)) != NULL) {
16313 long row_id = 0;
16314 char *row_label = NULL;
16315 char *escaped_name = NULL;
16316 char *escaped_label = NULL;
16317 if (!yyjson_is_obj(row) || !surface_context_match_runtime(row, related_scope_field, context_id) || !long_from_value_runtime(native_json_obj_get(row, "id"), &row_id)) {
16318 continue;
16319 }
16320 row_label = native_json_obj_get_string_dup(row, label_field);
16321 if (row_label == NULL || row_label[0] == '\0') {
16322 free(row_label);
16324 }
16325 if (json_array_contains_long_runtime(selected_root, row_id)) {
16326 escaped_name = html_escape_dup_runtime(checkbox_prefix != NULL ? checkbox_prefix : "field__");
16327 escaped_label = html_escape_dup_runtime(row_label != NULL ? row_label : "");
16328 if (escaped_name != NULL && escaped_label != NULL) {
16329 fprintf(stdout, "<label><input type=\"checkbox\" name=\"%s%ld\" value=\"%ld\" checked> %s</label>", escaped_name, row_id, row_id, escaped_label);
16330 emitted = 1;
16331 }
16332 free(escaped_name);
16333 free(escaped_label);
16334 } else {
16335 escaped_name = html_escape_dup_runtime(checkbox_prefix != NULL ? checkbox_prefix : "field__");
16336 escaped_label = html_escape_dup_runtime(row_label != NULL ? row_label : "");
16337 if (escaped_name != NULL && escaped_label != NULL) {
16338 fprintf(stdout, "<label><input type=\"checkbox\" name=\"%s%ld\" value=\"%ld\"> %s</label>", escaped_name, row_id, row_id, escaped_label);
16339 emitted = 1;
16340 }
16341 free(escaped_name);
16342 free(escaped_label);
16343 }
16344 free(row_label);
16345 }
16346 }
16347 if (!emitted && empty_options_text != NULL) {
16348 char *escaped_empty = html_escape_dup_runtime(empty_options_text);
16349 if (escaped_empty != NULL) {
16350 fprintf(stdout, "<p class=\"muted\">%s</p>", escaped_empty);
16351 free(escaped_empty);
16352 }
16353 }
16354 fprintf(stdout, "</div></div>");
16355 free(relation_field);
16356 free(related_collection);
16357 free(related_scope_field);
16358 free(label_field);
16359 free(checkbox_prefix);
16360 free(relation_label);
16361 free(relation_help_text);
16362 free(empty_options_text);
16363 native_json_doc_free(state_doc);
16364 native_json_doc_free(relation_doc);
16365 native_json_doc_free(context_doc);
16366 native_json_doc_free(selected_doc);
16367 return 0;
16368}
16369
16371 yyjson_val *admin = NULL;
16372 char *configured_label = NULL;
16373 char *delete_strategy = NULL;
16374 if (surface_root == NULL || !yyjson_is_obj(surface_root)) {
16375 return strdup("Delete");
16376 }
16377 admin = native_json_obj_get(surface_root, "admin");
16378 configured_label = native_json_obj_get_string_dup(admin, "delete_button_label");
16379 if (configured_label != NULL && configured_label[0] != '\0') {
16380 return configured_label;
16381 }
16382 free(configured_label);
16383 delete_strategy = generated_surface_delete_strategy_runtime(surface_root);
16384 if (delete_strategy != NULL && streq(delete_strategy, "soft")) {
16385 free(delete_strategy);
16386 return strdup("Deactivate");
16387 }
16388 free(delete_strategy);
16389 return strdup("Delete");
16390}
16391
16392static char *generated_surface_operation_path_dup_runtime(yyjson_val *surface_root, yyjson_val *context_root, const char *operation_name, const char *identity_value) {
16393 yyjson_val *operations = NULL;
16394 yyjson_val *operation_root = NULL;
16395 const char *template_path = NULL;
16396 yyjson_mut_doc *params_doc = NULL;
16397 yyjson_mut_val *params_root = NULL;
16398 yyjson_val *binding = NULL;
16399 char *context_param = NULL;
16400 char *context_value = NULL;
16401 char *identity_param = NULL;
16402 char *rendered = NULL;
16403 if (surface_root == NULL || context_root == NULL || operation_name == NULL || operation_name[0] == '\0' || !yyjson_is_obj(surface_root) || !yyjson_is_obj(context_root)) {
16404 return NULL;
16405 }
16406 operations = native_json_obj_get(surface_root, "operations");
16407 operation_root = native_json_obj_get(operations, operation_name);
16408 template_path = yyjson_get_str(native_json_obj_get(operation_root, "path"));
16409 if (template_path == NULL || template_path[0] == '\0') {
16410 return NULL;
16411 }
16412 params_doc = yyjson_mut_doc_new(NULL);
16413 params_root = params_doc != NULL ? yyjson_mut_obj(params_doc) : NULL;
16414 if (params_doc == NULL || params_root == NULL) {
16415 yyjson_mut_doc_free(params_doc);
16416 return NULL;
16417 }
16418 binding = native_json_obj_get(surface_root, "context_binding");
16419 context_param = native_json_obj_get_string_dup(binding, "param");
16420 context_value = json_value_tostring_dup_runtime(native_json_obj_get(context_root, "context_value"));
16421 identity_param = native_json_obj_get_string_dup(surface_root, "identity_param");
16422 yyjson_mut_doc_set_root(params_doc, params_root);
16423 if (context_param != NULL && context_param[0] != '\0' && context_value != NULL && context_value[0] != '\0') {
16424 yyjson_mut_obj_add_strcpy(params_doc, params_root, context_param, context_value);
16425 }
16426 if (identity_param != NULL && identity_param[0] != '\0' && identity_value != NULL && identity_value[0] != '\0') {
16427 yyjson_mut_obj_add_strcpy(params_doc, params_root, identity_param, identity_value);
16428 }
16429 rendered = render_path_template_text_dup_runtime(template_path, (yyjson_val *)params_root);
16430 free(context_param);
16431 free(context_value);
16432 free(identity_param);
16433 yyjson_mut_doc_free(params_doc);
16434 return rendered;
16435}
16436
16438 const char *surface_json = json_option_value_runtime(argc, argv, "--surface-json");
16439 const char *context_json = json_option_value_runtime(argc, argv, "--context-json");
16440 const char *list_result_json = json_option_value_runtime(argc, argv, "--list-result-json");
16441 yyjson_doc *surface_doc = NULL;
16442 yyjson_doc *context_doc = NULL;
16443 yyjson_doc *list_result_doc = NULL;
16444 yyjson_val *surface_root = load_root_from_text_runtime(surface_json, &surface_doc);
16445 yyjson_val *context_root = load_root_from_text_runtime(context_json, &context_doc);
16446 yyjson_val *list_result_root = load_root_from_text_runtime(list_result_json, &list_result_doc);
16447 yyjson_val *items = NULL;
16448 yyjson_mut_doc *out_doc = NULL;
16449 yyjson_mut_val *out_root = NULL;
16450 TextBufferRuntime header = {0};
16451 TextBufferRuntime rows = {0};
16452 yyjson_doc *columns_doc = NULL;
16453 yyjson_val *columns_root = NULL;
16454 yyjson_arr_iter iter;
16455 yyjson_val *item = NULL;
16456 char *columns_json = NULL;
16457 char *delete_button_label = NULL;
16458 char *identity_field = NULL;
16459 char *empty_state_text = NULL;
16460 size_t column_count = 1;
16461 int code = 64;
16462 if (surface_root == NULL || context_root == NULL || list_result_root == NULL || !yyjson_is_obj(surface_root) || !yyjson_is_obj(context_root) || !yyjson_is_obj(list_result_root)) {
16463 goto generated_surface_manage_table_cleanup;
16464 }
16465 items = native_json_obj_get_array(list_result_root, "items");
16466 columns_json = native_json_serialize_compact_dup(native_json_obj_get(surface_root, "admin"));
16467 free(columns_json);
16468 columns_json = NULL;
16469 {
16470 yyjson_mut_doc *tmp_doc = yyjson_mut_doc_new(NULL);
16471 yyjson_mut_val *tmp_array = NULL;
16472 yyjson_val *admin = NULL;
16473 yyjson_val *list_columns = NULL;
16474 yyjson_val *response_fields = NULL;
16475 yyjson_arr_iter field_iter;
16476 yyjson_val *field = NULL;
16477 size_t emitted = 0;
16478 admin = native_json_obj_get(surface_root, "admin");
16479 list_columns = native_json_obj_get(admin, "list_columns");
16480 if (list_columns != NULL && yyjson_is_arr(list_columns)) {
16481 columns_json = native_json_serialize_compact_dup(list_columns);
16482 yyjson_mut_doc_free(tmp_doc);
16483 } else {
16484 tmp_array = tmp_doc != NULL ? yyjson_mut_arr(tmp_doc) : NULL;
16485 response_fields = native_json_obj_get_array(surface_root, "response_fields");
16486 if (tmp_doc == NULL || tmp_array == NULL) {
16487 yyjson_mut_doc_free(tmp_doc);
16488 goto generated_surface_manage_table_cleanup;
16489 }
16490 yyjson_mut_doc_set_root(tmp_doc, tmp_array);
16491 if (response_fields != NULL && yyjson_arr_iter_init(response_fields, &field_iter)) {
16492 while ((field = yyjson_arr_iter_next(&field_iter)) != NULL) {
16493 const char *field_name = yyjson_is_str(field) ? yyjson_get_str(field) : NULL;
16494 yyjson_mut_val *entry = NULL;
16495 char *label = NULL;
16496 if (field_name == NULL || streq(field_name, "id") || streq(field_name, "business_id")) {
16497 continue;
16498 }
16499 if (emitted >= 4) {
16500 break;
16501 }
16503 entry = yyjson_mut_obj(tmp_doc);
16504 if (entry != NULL && label != NULL && yyjson_mut_obj_add_strcpy(tmp_doc, entry, "field", field_name) && yyjson_mut_obj_add_strcpy(tmp_doc, entry, "label", label)) {
16505 yyjson_mut_arr_append(tmp_array, entry);
16506 emitted++;
16507 }
16508 free(label);
16509 }
16510 }
16511 columns_json = yyjson_mut_val_write(tmp_array, YYJSON_WRITE_NOFLAG, NULL);
16512 yyjson_mut_doc_free(tmp_doc);
16513 }
16514 }
16515 columns_root = load_root_from_text_runtime(columns_json != NULL ? columns_json : "[]", &columns_doc);
16516 if (columns_root == NULL || !yyjson_is_arr(columns_root)) {
16517 goto generated_surface_manage_table_cleanup;
16518 }
16519 delete_button_label = generated_surface_delete_button_label_dup_runtime(surface_root);
16520 identity_field = native_json_obj_get_string_dup(surface_root, "identity_field");
16521 empty_state_text = native_json_obj_get_string_dup(native_json_obj_get(surface_root, "admin"), "empty_state_text");
16522 if (identity_field == NULL || identity_field[0] == '\0') {
16523 free(identity_field);
16524 identity_field = strdup("id");
16525 }
16526 if (empty_state_text == NULL || empty_state_text[0] == '\0') {
16527 free(empty_state_text);
16528 empty_state_text = strdup("No records found.");
16529 }
16530 column_count = yyjson_arr_size(columns_root) + 1;
16531 if (yyjson_arr_iter_init(columns_root, &iter)) {
16532 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
16533 char *column_label = native_json_obj_get_string_dup(item, "label");
16534 char *field_name = native_json_obj_get_string_dup(item, "field");
16535 if ((column_label == NULL || column_label[0] == '\0') && field_name != NULL && field_name[0] != '\0') {
16536 free(column_label);
16537 column_label = strdup(field_name);
16538 }
16539 if (column_label == NULL || column_label[0] == '\0') {
16540 free(column_label);
16541 column_label = strdup("value");
16542 }
16543 if (column_label == NULL || !text_buffer_append_text_runtime(&header, "<th>") || !text_buffer_append_html_escaped_runtime(&header, column_label) || !text_buffer_append_text_runtime(&header, "</th>")) {
16544 free(column_label);
16545 goto generated_surface_manage_table_cleanup;
16546 }
16547 free(column_label);
16548 free(field_name);
16549 }
16550 }
16551 if (items != NULL && yyjson_is_arr(items) && yyjson_arr_iter_init(items, &iter)) {
16552 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
16553 yyjson_arr_iter col_iter;
16554 yyjson_val *column = NULL;
16555 char *item_id = NULL;
16556 char *edit_path = NULL;
16557 char *delete_path = NULL;
16558 if (!yyjson_is_obj(item)) {
16559 continue;
16560 }
16561 item_id = json_value_tostring_dup_runtime(native_json_obj_get(item, identity_field));
16562 edit_path = generated_surface_operation_path_dup_runtime(surface_root, context_root, "edit", item_id);
16563 delete_path = generated_surface_operation_path_dup_runtime(surface_root, context_root, "delete", item_id);
16564 if (!text_buffer_append_text_runtime(&rows, "<tr>")) {
16565 free(item_id); free(edit_path); free(delete_path); goto generated_surface_manage_table_cleanup;
16566 }
16567 if (yyjson_arr_iter_init(columns_root, &col_iter)) {
16568 while ((column = yyjson_arr_iter_next(&col_iter)) != NULL) {
16569 char *field_name = native_json_obj_get_string_dup(column, "field");
16570 char *field_value = generated_surface_display_value_dup_runtime(item, field_name);
16571 int ok = text_buffer_append_text_runtime(&rows, "<td>") && text_buffer_append_html_escaped_runtime(&rows, field_value != NULL ? field_value : "") && text_buffer_append_text_runtime(&rows, "</td>");
16572 free(field_name);
16573 free(field_value);
16574 if (!ok) {
16575 free(item_id); free(edit_path); free(delete_path); goto generated_surface_manage_table_cleanup;
16576 }
16577 }
16578 }
16579 if (!text_buffer_append_text_runtime(&rows, "<td>")) {
16580 free(item_id); free(edit_path); free(delete_path); goto generated_surface_manage_table_cleanup;
16581 }
16582 if (edit_path != NULL && edit_path[0] != '\0') {
16583 if (!text_buffer_append_text_runtime(&rows, "<a class=\"btn btn-secondary\" href=\"") || !text_buffer_append_html_escaped_runtime(&rows, edit_path) || !text_buffer_append_text_runtime(&rows, "\">Edit</a> ")) {
16584 free(item_id); free(edit_path); free(delete_path); goto generated_surface_manage_table_cleanup;
16585 }
16586 }
16587 if (delete_path != NULL && delete_path[0] != '\0') {
16588 if (!text_buffer_append_text_runtime(&rows, "<form method=\"post\" action=\"") || !text_buffer_append_html_escaped_runtime(&rows, delete_path) || !text_buffer_append_text_runtime(&rows, "\" class=\"inline-form\"><button type=\"submit\" class=\"btn btn-danger\">") || !text_buffer_append_html_escaped_runtime(&rows, delete_button_label != NULL ? delete_button_label : "Delete") || !text_buffer_append_text_runtime(&rows, "</button></form>")) {
16589 free(item_id); free(edit_path); free(delete_path); goto generated_surface_manage_table_cleanup;
16590 }
16591 }
16592 if (!text_buffer_append_text_runtime(&rows, "</td></tr>")) {
16593 free(item_id); free(edit_path); free(delete_path); goto generated_surface_manage_table_cleanup;
16594 }
16595 free(item_id);
16596 free(edit_path);
16597 free(delete_path);
16598 }
16599 }
16600 if (rows.length == 0) {
16601 if (!text_buffer_append_format_runtime(&rows, "<tr><td colspan=\"%lu\">", (unsigned long)column_count) || !text_buffer_append_html_escaped_runtime(&rows, empty_state_text != NULL ? empty_state_text : "No records found.") || !text_buffer_append_text_runtime(&rows, "</td></tr>")) {
16602 goto generated_surface_manage_table_cleanup;
16603 }
16604 }
16605 out_doc = yyjson_mut_doc_new(NULL);
16606 out_root = out_doc != NULL ? yyjson_mut_obj(out_doc) : NULL;
16607 if (out_doc == NULL || out_root == NULL) {
16608 goto generated_surface_manage_table_cleanup;
16609 }
16610 yyjson_mut_doc_set_root(out_doc, out_root);
16611 yyjson_mut_obj_add_strcpy(out_doc, out_root, "header_html", header.text != NULL ? header.text : "");
16612 yyjson_mut_obj_add_strcpy(out_doc, out_root, "row_html", rows.text != NULL ? rows.text : "");
16613 yyjson_mut_obj_add_int(out_doc, out_root, "column_count", (long)column_count);
16614 code = emit_compact_json_mut_value_runtime(out_root);
16615generated_surface_manage_table_cleanup:
16616 free(columns_json);
16617 free(delete_button_label);
16618 free(identity_field);
16619 free(empty_state_text);
16620 text_buffer_free_runtime(&header);
16622 native_json_doc_free(surface_doc);
16623 native_json_doc_free(context_doc);
16624 native_json_doc_free(list_result_doc);
16625 native_json_doc_free(columns_doc);
16626 yyjson_mut_doc_free(out_doc);
16627 return code;
16628}
16629
16630static int handle_generated_surface_form_fields_html_command(int argc, char **argv) {
16631 const char *state_path = json_option_value_runtime(argc, argv, "--state");
16632 const char *fixture_path = json_option_value_runtime(argc, argv, "--fixture");
16633 const char *surface_json = json_option_value_runtime(argc, argv, "--surface-json");
16634 const char *context_json = json_option_value_runtime(argc, argv, "--context-json");
16635 const char *item_json = json_option_value_runtime(argc, argv, "--item-json");
16636 yyjson_doc *state_doc = NULL;
16637 yyjson_doc *fixture_doc = NULL;
16638 yyjson_doc *surface_doc = NULL;
16639 yyjson_doc *context_doc = NULL;
16640 yyjson_doc *item_doc = NULL;
16641 yyjson_val *state_root = NULL;
16642 yyjson_val *fixture_root = NULL;
16643 yyjson_val *surface_root = NULL;
16644 yyjson_val *context_root = NULL;
16645 yyjson_val *item_root = NULL;
16646 char *html = NULL;
16647 int code = 64;
16648 if (state_path == NULL || fixture_path == NULL || surface_json == NULL || context_json == NULL) {
16649 return 64;
16650 }
16651 state_doc = native_json_doc_load_file(state_path);
16652 fixture_doc = native_json_doc_load_file(fixture_path);
16653 surface_root = load_root_from_text_runtime(surface_json, &surface_doc);
16654 context_root = load_root_from_text_runtime(context_json, &context_doc);
16655 item_root = load_root_from_text_runtime(item_json != NULL ? item_json : "{}", &item_doc);
16656 state_root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
16657 fixture_root = fixture_doc != NULL ? yyjson_doc_get_root(fixture_doc) : NULL;
16658 if (state_root == NULL || fixture_root == NULL || surface_root == NULL || context_root == NULL || item_root == NULL || !yyjson_is_obj(state_root) || !yyjson_is_obj(fixture_root) || !yyjson_is_obj(surface_root) || !yyjson_is_obj(context_root) || !yyjson_is_obj(item_root)) {
16659 code = 64;
16660 goto generated_surface_form_fields_cleanup;
16661 }
16662 html = build_generated_surface_form_fields_html_runtime(state_root, fixture_root, surface_root, context_root, item_root);
16663 if (html == NULL) {
16664 code = 69;
16665 goto generated_surface_form_fields_cleanup;
16666 }
16667 fputs(html, stdout);
16668 code = 0;
16669generated_surface_form_fields_cleanup:
16670 free(html);
16671 native_json_doc_free(state_doc);
16672 native_json_doc_free(fixture_doc);
16673 native_json_doc_free(surface_doc);
16674 native_json_doc_free(context_doc);
16675 native_json_doc_free(item_doc);
16676 return code;
16677}
16678
16680 const char *surface_json = json_option_value_runtime(argc, argv, "--surface-json");
16681 const char *raw_query = json_option_value_runtime(argc, argv, "--query");
16682 yyjson_doc *surface_doc = NULL;
16683 yyjson_val *surface_root = load_root_from_text_runtime(surface_json, &surface_doc);
16684 char *html = NULL;
16685 int code = 64;
16686 if (surface_root == NULL || !yyjson_is_obj(surface_root)) {
16687 native_json_doc_free(surface_doc);
16688 return 64;
16689 }
16690 html = build_generated_surface_filter_controls_html_runtime(surface_root, raw_query != NULL ? raw_query : "");
16691 if (html == NULL) {
16692 code = 69;
16693 } else {
16694 fputs(html, stdout);
16695 code = 0;
16696 }
16697 free(html);
16698 native_json_doc_free(surface_doc);
16699 return code;
16700}
16701
16703 const char *surface_json = json_option_value_runtime(argc, argv, "--surface-json");
16704 const char *raw_query = json_option_value_runtime(argc, argv, "--query");
16705 yyjson_doc *surface_doc = NULL;
16706 yyjson_val *surface_root = load_root_from_text_runtime(surface_json, &surface_doc);
16707 char *html = NULL;
16708 int code = 64;
16709 if (surface_root == NULL || !yyjson_is_obj(surface_root)) {
16710 native_json_doc_free(surface_doc);
16711 return 64;
16712 }
16713 html = build_generated_surface_sort_options_html_runtime(surface_root, raw_query != NULL ? raw_query : "");
16714 if (html == NULL) {
16715 code = 69;
16716 } else {
16717 fputs(html, stdout);
16718 code = 0;
16719 }
16720 free(html);
16721 native_json_doc_free(surface_doc);
16722 return code;
16723}
16724
16725static int handle_generated_surface_manage_page_json_command(int argc, char **argv) {
16726 const char *state_path = json_option_value_runtime(argc, argv, "--state");
16727 const char *fixture_path = json_option_value_runtime(argc, argv, "--fixture");
16728 const char *surface_json = json_option_value_runtime(argc, argv, "--surface-json");
16729 const char *context_json = json_option_value_runtime(argc, argv, "--context-json");
16730 const char *raw_query = json_option_value_runtime(argc, argv, "--query");
16731 yyjson_doc *state_doc = NULL;
16732 yyjson_doc *fixture_doc = NULL;
16733 yyjson_doc *surface_doc = NULL;
16734 yyjson_doc *context_doc = NULL;
16735 yyjson_doc *empty_item_doc = NULL;
16736 yyjson_val *state_root = NULL;
16737 yyjson_val *fixture_root = NULL;
16738 yyjson_val *surface_root = NULL;
16739 yyjson_val *context_root = NULL;
16740 yyjson_val *empty_item_root = NULL;
16741 yyjson_val *items_array = NULL;
16742 yyjson_val *context_id_value = NULL;
16743 yyjson_mut_doc *out_doc = NULL;
16744 yyjson_mut_val *out_root = NULL;
16745 yyjson_mut_val *items_out = NULL;
16746 yyjson_mut_val *pagination = NULL;
16747 yyjson_mut_val *filters = NULL;
16748 yyjson_mut_val *sort_config = NULL;
16749 json_value_list_runtime filtered = {0};
16750 yyjson_arr_iter iter;
16751 yyjson_val *item = NULL;
16752 char *collection_name = NULL;
16753 char *scope_field = NULL;
16754 char *sort_key = NULL;
16755 char *manage_path = NULL;
16756 char *create_path = NULL;
16757 char *back_path_template = NULL;
16758 char *back_path = NULL;
16759 char *manage_table_json = NULL;
16760 yyjson_doc *manage_table_doc = NULL;
16761 yyjson_val *manage_table_root = NULL;
16762 char *header_html = NULL;
16763 char *row_html = NULL;
16764 char *form_fields_html = NULL;
16765 char *title = NULL;
16766 char *intro = NULL;
16767 char *filter_title = NULL;
16768 char *records_title = NULL;
16769 char *create_title = NULL;
16770 char *create_button_label = NULL;
16771 char *filter_controls_html = NULL;
16772 char *sort_options_html = NULL;
16773 char *current_page_size = NULL;
16774 char *page_size_text = NULL;
16775 char *pagination_summary = NULL;
16776 long page_default = 25;
16777 long page_max = 100;
16778 long page_value = 1;
16779 long page_size_value = 25;
16780 size_t total_items = 0;
16781 size_t start_index = 0;
16782 size_t end_index = 0;
16783 int code = 69;
16784 if (state_path == NULL || fixture_path == NULL || surface_json == NULL || context_json == NULL) {
16785 return 64;
16786 }
16787 state_doc = native_json_doc_load_file(state_path);
16788 fixture_doc = native_json_doc_load_file(fixture_path);
16789 surface_root = load_root_from_text_runtime(surface_json, &surface_doc);
16790 context_root = load_root_from_text_runtime(context_json, &context_doc);
16791 state_root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
16792 fixture_root = fixture_doc != NULL ? yyjson_doc_get_root(fixture_doc) : NULL;
16793 if (state_root == NULL || fixture_root == NULL || surface_root == NULL || context_root == NULL || !yyjson_is_obj(state_root) || !yyjson_is_obj(fixture_root) || !yyjson_is_obj(surface_root) || !yyjson_is_obj(context_root)) {
16794 code = 64;
16795 goto generated_surface_manage_page_cleanup;
16796 }
16797 collection_name = native_json_obj_get_string_dup(surface_root, "entity_collection");
16798 scope_field = native_json_obj_get_string_dup(surface_root, "scope_field");
16799 context_id_value = native_json_obj_get(context_root, "context_id");
16800 items_array = collection_name != NULL ? state_array_runtime(state_root, collection_name) : NULL;
16801 if (items_array != NULL && yyjson_arr_iter_init(items_array, &iter)) {
16802 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
16803 if (!yyjson_is_obj(item) || !surface_context_match_runtime(item, scope_field, context_id_value)) {
16804 continue;
16805 }
16806 if (!json_value_list_append_runtime(&filtered, item)) {
16807 goto generated_surface_manage_page_cleanup;
16808 }
16809 }
16810 }
16811 out_doc = yyjson_mut_doc_new(NULL);
16812 out_root = out_doc != NULL ? yyjson_mut_obj(out_doc) : NULL;
16813 if (out_doc == NULL || out_root == NULL) {
16814 goto generated_surface_manage_page_cleanup;
16815 }
16816 items_out = yyjson_mut_arr(out_doc);
16817 pagination = yyjson_mut_obj(out_doc);
16818 filters = surface_build_active_filters_runtime(out_doc, surface_root, raw_query != NULL ? raw_query : "");
16819 sort_config = surface_build_sort_config_runtime(out_doc, surface_root, raw_query != NULL ? raw_query : "");
16820 if (items_out == NULL || pagination == NULL || filters == NULL || sort_config == NULL) {
16821 goto generated_surface_manage_page_cleanup;
16822 }
16823 page_default = native_json_obj_get_long_default(surface_root, "page_size_default", 25, NULL);
16824 page_max = native_json_obj_get_long_default(surface_root, "page_size_max", 100, NULL);
16825 {
16826 char *page_text = query_value_decoded_dup_runtime(raw_query != NULL ? raw_query : "", "page");
16827 page_size_text = query_value_decoded_dup_runtime(raw_query != NULL ? raw_query : "", "page_size");
16828 sort_key = query_value_decoded_dup_runtime(raw_query != NULL ? raw_query : "", "sort");
16829 if (page_text != NULL && digits_only_text_runtime(page_text) && strtol(page_text, NULL, 10) > 0) {
16830 page_value = strtol(page_text, NULL, 10);
16831 }
16832 if (page_size_text != NULL && digits_only_text_runtime(page_size_text) && strtol(page_size_text, NULL, 10) > 0) {
16833 page_size_value = strtol(page_size_text, NULL, 10);
16834 } else {
16835 page_size_value = page_default;
16836 }
16837 if (page_size_value > page_max) {
16838 page_size_value = page_max;
16839 }
16840 free(page_text);
16841 }
16842 {
16843 json_value_list_runtime visible = {0};
16844 size_t idx = 0;
16845 for (idx = 0; idx < filtered.count; idx++) {
16846 if (surface_filter_item_runtime(filtered.items[idx], (yyjson_val *)filters) && json_value_list_append_runtime(&visible, filtered.items[idx])) {
16847 }
16848 }
16850 filtered = visible;
16851 }
16852 surface_sort_items_runtime(&filtered, (yyjson_val *)sort_config);
16853 total_items = filtered.count;
16854 start_index = (size_t)((page_value - 1) * page_size_value);
16855 if (start_index > total_items) {
16856 start_index = total_items;
16857 }
16858 end_index = start_index + (size_t)page_size_value;
16859 if (end_index > total_items) {
16860 end_index = total_items;
16861 }
16862 {
16863 yyjson_val *response_fields = native_json_obj_get_array(surface_root, "response_fields");
16864 size_t idx = 0;
16865 for (idx = start_index; idx < end_index; idx++) {
16866 yyjson_mut_val *entry = NULL;
16867 if (response_fields != NULL && yyjson_is_arr(response_fields) && yyjson_arr_size(response_fields) > 0) {
16868 yyjson_arr_iter field_iter;
16869 yyjson_val *field = NULL;
16870 entry = yyjson_mut_obj(out_doc);
16871 if (entry != NULL && yyjson_arr_iter_init(response_fields, &field_iter)) {
16872 while ((field = yyjson_arr_iter_next(&field_iter)) != NULL) {
16873 if (yyjson_is_str(field)) {
16874 const char *field_name = yyjson_get_str(field);
16875 yyjson_val *value = native_json_obj_get(filtered.items[idx], field_name);
16876 yyjson_mut_obj_put(entry, yyjson_mut_strcpy(out_doc, field_name), value != NULL ? yyjson_val_mut_copy(out_doc, value) : yyjson_mut_null(out_doc));
16877 }
16878 }
16879 }
16880 } else {
16881 entry = yyjson_val_mut_copy(out_doc, filtered.items[idx]);
16882 }
16883 if (entry != NULL) {
16884 yyjson_mut_arr_append(items_out, entry);
16885 }
16886 }
16887 }
16888 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "items"), items_out);
16889 yyjson_mut_obj_add_int(out_doc, pagination, "page", page_value);
16890 yyjson_mut_obj_add_int(out_doc, pagination, "page_size", page_size_value);
16891 yyjson_mut_obj_add_int(out_doc, pagination, "total_items", (long)total_items);
16892 yyjson_mut_obj_add_int(out_doc, pagination, "total_pages", page_size_value > 0 ? (long)((total_items + (size_t)page_size_value - 1) / (size_t)page_size_value) : 1);
16893 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "pagination"), pagination);
16894 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "sort"), (sort_key != NULL && sort_key[0] != '\0') ? yyjson_mut_strcpy(out_doc, sort_key) : yyjson_mut_null(out_doc));
16895 manage_path = generated_surface_operation_path_dup_runtime(surface_root, context_root, "manage", NULL);
16896 create_path = generated_surface_operation_path_dup_runtime(surface_root, context_root, "create", NULL);
16897 back_path_template = native_json_obj_get_string_dup(native_json_obj_get(surface_root, "admin"), "back_path_template");
16898 if (back_path_template != NULL && back_path_template[0] != '\0') {
16899 yyjson_mut_doc *params_doc = yyjson_mut_doc_new(NULL);
16900 yyjson_mut_val *params_root = params_doc != NULL ? yyjson_mut_obj(params_doc) : NULL;
16901 yyjson_val *binding = native_json_obj_get(surface_root, "context_binding");
16902 char *context_param = native_json_obj_get_string_dup(binding, "param");
16903 char *context_value = json_value_tostring_dup_runtime(native_json_obj_get(context_root, "context_value"));
16904 if (params_doc != NULL && params_root != NULL) {
16905 yyjson_mut_doc_set_root(params_doc, params_root);
16906 if (context_param != NULL && context_param[0] != '\0' && context_value != NULL && context_value[0] != '\0') {
16907 yyjson_mut_obj_add_strcpy(params_doc, params_root, context_param, context_value);
16908 }
16909 back_path = render_path_template_text_dup_runtime(back_path_template, (yyjson_val *)params_root);
16910 }
16911 free(context_param);
16912 free(context_value);
16913 yyjson_mut_doc_free(params_doc);
16914 }
16915 if (back_path == NULL || back_path[0] == '\0') {
16916 free(back_path);
16917 back_path = manage_path != NULL ? strdup(manage_path) : strdup("");
16918 }
16919 {
16920 char *list_result_json = native_json_serialize_compact_dup((yyjson_val *)out_root);
16921 manage_table_json = list_result_json != NULL ? NULL : NULL;
16922 if (list_result_json == NULL) {
16923 goto generated_surface_manage_page_cleanup;
16924 }
16925 {
16926 yyjson_doc *surface_doc2 = NULL;
16927 yyjson_doc *context_doc2 = NULL;
16928 yyjson_doc *list_doc2 = NULL;
16929 yyjson_val *surface_root2 = load_root_from_text_runtime(surface_json, &surface_doc2);
16930 yyjson_val *context_root2 = load_root_from_text_runtime(context_json, &context_doc2);
16931 yyjson_val *list_root2 = load_root_from_text_runtime(list_result_json, &list_doc2);
16932 TextBufferRuntime header = {0};
16933 TextBufferRuntime rows = {0};
16934 yyjson_doc *columns_doc = NULL;
16935 yyjson_val *columns_root = NULL;
16936 yyjson_mut_doc *tmp_doc = NULL;
16937 char *columns_json = NULL;
16938 char *delete_button_label = NULL;
16939 char *identity_field = NULL;
16940 char *empty_state_text = NULL;
16941 size_t column_count = 1;
16942 if (surface_root2 == NULL || context_root2 == NULL || list_root2 == NULL) {
16943 free(list_result_json);
16944 native_json_doc_free(surface_doc2);
16945 native_json_doc_free(context_doc2);
16946 native_json_doc_free(list_doc2);
16947 goto generated_surface_manage_page_cleanup;
16948 }
16949 {
16950 yyjson_val *admin = native_json_obj_get(surface_root2, "admin");
16951 yyjson_val *list_columns = native_json_obj_get(admin, "list_columns");
16952 yyjson_val *response_fields = NULL;
16953 if (list_columns != NULL && yyjson_is_arr(list_columns)) {
16954 columns_json = native_json_serialize_compact_dup(list_columns);
16955 } else {
16956 yyjson_mut_val *tmp_array = NULL;
16957 yyjson_arr_iter field_iter;
16958 yyjson_val *field = NULL;
16959 size_t emitted = 0;
16960 tmp_doc = yyjson_mut_doc_new(NULL);
16961 tmp_array = tmp_doc != NULL ? yyjson_mut_arr(tmp_doc) : NULL;
16962 response_fields = native_json_obj_get_array(surface_root2, "response_fields");
16963 if (tmp_doc == NULL || tmp_array == NULL) {
16964 free(list_result_json);
16965 native_json_doc_free(surface_doc2);
16966 native_json_doc_free(context_doc2);
16967 native_json_doc_free(list_doc2);
16968 yyjson_mut_doc_free(tmp_doc);
16969 goto generated_surface_manage_page_cleanup;
16970 }
16971 yyjson_mut_doc_set_root(tmp_doc, tmp_array);
16972 if (response_fields != NULL && yyjson_arr_iter_init(response_fields, &field_iter)) {
16973 while ((field = yyjson_arr_iter_next(&field_iter)) != NULL) {
16974 const char *field_name = yyjson_is_str(field) ? yyjson_get_str(field) : NULL;
16975 yyjson_mut_val *entry = NULL;
16976 char *label = NULL;
16977 if (field_name == NULL || streq(field_name, "id") || streq(field_name, "business_id")) {
16978 continue;
16979 }
16980 if (emitted >= 4) {
16981 break;
16982 }
16984 entry = yyjson_mut_obj(tmp_doc);
16985 if (entry != NULL && label != NULL && yyjson_mut_obj_add_strcpy(tmp_doc, entry, "field", field_name) && yyjson_mut_obj_add_strcpy(tmp_doc, entry, "label", label)) {
16986 yyjson_mut_arr_append(tmp_array, entry);
16987 emitted++;
16988 }
16989 free(label);
16990 }
16991 }
16992 columns_json = yyjson_mut_val_write(tmp_array, YYJSON_WRITE_NOFLAG, NULL);
16993 }
16994 }
16995 columns_root = load_root_from_text_runtime(columns_json != NULL ? columns_json : "[]", &columns_doc);
16996 delete_button_label = generated_surface_delete_button_label_dup_runtime(surface_root2);
16997 identity_field = native_json_obj_get_string_dup(surface_root2, "identity_field");
16998 empty_state_text = native_json_obj_get_string_dup(native_json_obj_get(surface_root2, "admin"), "empty_state_text");
16999 if (identity_field == NULL || identity_field[0] == '\0') {
17000 free(identity_field);
17001 identity_field = strdup("id");
17002 }
17003 if (empty_state_text == NULL || empty_state_text[0] == '\0') {
17004 free(empty_state_text);
17005 empty_state_text = strdup("No records found.");
17006 }
17007 if (columns_root != NULL && yyjson_is_arr(columns_root)) {
17008 column_count = yyjson_arr_size(columns_root) + 1;
17009 if (yyjson_arr_iter_init(columns_root, &iter)) {
17010 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
17011 char *column_label = native_json_obj_get_string_dup(item, "label");
17012 char *field_name = native_json_obj_get_string_dup(item, "field");
17013 if ((column_label == NULL || column_label[0] == '\0') && field_name != NULL && field_name[0] != '\0') {
17014 free(column_label);
17015 column_label = strdup(field_name);
17016 }
17017 if (column_label == NULL || column_label[0] == '\0') {
17018 free(column_label);
17019 column_label = strdup("value");
17020 }
17021 if (column_label == NULL || !text_buffer_append_text_runtime(&header, "<th>") || !text_buffer_append_html_escaped_runtime(&header, column_label) || !text_buffer_append_text_runtime(&header, "</th>")) {
17022 free(column_label);
17023 free(field_name);
17024 text_buffer_free_runtime(&header);
17026 free(list_result_json);
17027 free(columns_json);
17028 free(delete_button_label);
17029 free(identity_field);
17030 free(empty_state_text);
17031 native_json_doc_free(surface_doc2);
17032 native_json_doc_free(context_doc2);
17033 native_json_doc_free(list_doc2);
17034 native_json_doc_free(columns_doc);
17035 yyjson_mut_doc_free(tmp_doc);
17036 goto generated_surface_manage_page_cleanup;
17037 }
17038 free(column_label);
17039 free(field_name);
17040 }
17041 }
17042 }
17043 item = NULL;
17044 if (yyjson_arr_iter_init(native_json_obj_get_array(list_root2, "items"), &iter)) {
17045 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
17046 yyjson_arr_iter col_iter;
17047 yyjson_val *column = NULL;
17048 char *item_id = NULL;
17049 char *edit_path = NULL;
17050 char *delete_path = NULL;
17051 if (!yyjson_is_obj(item)) {
17052 continue;
17053 }
17054 item_id = json_value_tostring_dup_runtime(native_json_obj_get(item, identity_field));
17055 edit_path = generated_surface_operation_path_dup_runtime(surface_root2, context_root2, "edit", item_id);
17056 delete_path = generated_surface_operation_path_dup_runtime(surface_root2, context_root2, "delete", item_id);
17057 if (!text_buffer_append_text_runtime(&rows, "<tr>")) {
17058 free(item_id); free(edit_path); free(delete_path);
17060 free(list_result_json); free(columns_json); free(delete_button_label); free(identity_field); free(empty_state_text);
17061 native_json_doc_free(surface_doc2); native_json_doc_free(context_doc2); native_json_doc_free(list_doc2); native_json_doc_free(columns_doc); yyjson_mut_doc_free(tmp_doc);
17062 goto generated_surface_manage_page_cleanup;
17063 }
17064 if (columns_root != NULL && yyjson_arr_iter_init(columns_root, &col_iter)) {
17065 while ((column = yyjson_arr_iter_next(&col_iter)) != NULL) {
17066 char *field_name = native_json_obj_get_string_dup(column, "field");
17067 char *field_value = generated_surface_display_value_dup_runtime(item, field_name);
17068 int ok = text_buffer_append_text_runtime(&rows, "<td>") && text_buffer_append_html_escaped_runtime(&rows, field_value != NULL ? field_value : "") && text_buffer_append_text_runtime(&rows, "</td>");
17069 free(field_name);
17070 free(field_value);
17071 if (!ok) {
17072 free(item_id); free(edit_path); free(delete_path);
17074 free(list_result_json); free(columns_json); free(delete_button_label); free(identity_field); free(empty_state_text);
17075 native_json_doc_free(surface_doc2); native_json_doc_free(context_doc2); native_json_doc_free(list_doc2); native_json_doc_free(columns_doc); yyjson_mut_doc_free(tmp_doc);
17076 goto generated_surface_manage_page_cleanup;
17077 }
17078 }
17079 }
17080 if (!text_buffer_append_text_runtime(&rows, "<td>")) {
17081 free(item_id); free(edit_path); free(delete_path);
17083 free(list_result_json); free(columns_json); free(delete_button_label); free(identity_field); free(empty_state_text);
17084 native_json_doc_free(surface_doc2); native_json_doc_free(context_doc2); native_json_doc_free(list_doc2); native_json_doc_free(columns_doc); yyjson_mut_doc_free(tmp_doc);
17085 goto generated_surface_manage_page_cleanup;
17086 }
17087 if (edit_path != NULL && edit_path[0] != '\0') {
17088 if (!text_buffer_append_text_runtime(&rows, "<a class=\"btn btn-secondary\" href=\"") || !text_buffer_append_html_escaped_runtime(&rows, edit_path) || !text_buffer_append_text_runtime(&rows, "\">Edit</a> ")) {
17089 free(item_id); free(edit_path); free(delete_path);
17091 free(list_result_json); free(columns_json); free(delete_button_label); free(identity_field); free(empty_state_text);
17092 native_json_doc_free(surface_doc2); native_json_doc_free(context_doc2); native_json_doc_free(list_doc2); native_json_doc_free(columns_doc); yyjson_mut_doc_free(tmp_doc);
17093 goto generated_surface_manage_page_cleanup;
17094 }
17095 }
17096 if (delete_path != NULL && delete_path[0] != '\0') {
17097 if (!text_buffer_append_text_runtime(&rows, "<form method=\"post\" action=\"") || !text_buffer_append_html_escaped_runtime(&rows, delete_path) || !text_buffer_append_text_runtime(&rows, "\" class=\"inline-form\"><button type=\"submit\" class=\"btn btn-danger\">") || !text_buffer_append_html_escaped_runtime(&rows, delete_button_label != NULL ? delete_button_label : "Delete") || !text_buffer_append_text_runtime(&rows, "</button></form>")) {
17098 free(item_id); free(edit_path); free(delete_path);
17100 free(list_result_json); free(columns_json); free(delete_button_label); free(identity_field); free(empty_state_text);
17101 native_json_doc_free(surface_doc2); native_json_doc_free(context_doc2); native_json_doc_free(list_doc2); native_json_doc_free(columns_doc); yyjson_mut_doc_free(tmp_doc);
17102 goto generated_surface_manage_page_cleanup;
17103 }
17104 }
17105 if (!text_buffer_append_text_runtime(&rows, "</td></tr>")) {
17106 free(item_id); free(edit_path); free(delete_path);
17108 free(list_result_json); free(columns_json); free(delete_button_label); free(identity_field); free(empty_state_text);
17109 native_json_doc_free(surface_doc2); native_json_doc_free(context_doc2); native_json_doc_free(list_doc2); native_json_doc_free(columns_doc); yyjson_mut_doc_free(tmp_doc);
17110 goto generated_surface_manage_page_cleanup;
17111 }
17112 free(item_id);
17113 free(edit_path);
17114 free(delete_path);
17115 }
17116 }
17117 if (rows.length == 0) {
17118 if (!text_buffer_append_format_runtime(&rows, "<tr><td colspan=\"%lu\">", (unsigned long)column_count) || !text_buffer_append_html_escaped_runtime(&rows, empty_state_text) || !text_buffer_append_text_runtime(&rows, "</td></tr>")) {
17120 free(list_result_json); free(columns_json); free(delete_button_label); free(identity_field); free(empty_state_text);
17121 native_json_doc_free(surface_doc2); native_json_doc_free(context_doc2); native_json_doc_free(list_doc2); native_json_doc_free(columns_doc); yyjson_mut_doc_free(tmp_doc);
17122 goto generated_surface_manage_page_cleanup;
17123 }
17124 }
17125 header_html = text_buffer_take_runtime(&header);
17126 row_html = text_buffer_take_runtime(&rows);
17127 free(list_result_json);
17128 free(columns_json);
17129 free(delete_button_label);
17130 free(identity_field);
17131 free(empty_state_text);
17132 native_json_doc_free(surface_doc2);
17133 native_json_doc_free(context_doc2);
17134 native_json_doc_free(list_doc2);
17135 native_json_doc_free(columns_doc);
17136 yyjson_mut_doc_free(tmp_doc);
17137 }
17138 }
17139 empty_item_root = load_root_from_text_runtime("{}", &empty_item_doc);
17140 form_fields_html = (empty_item_root != NULL) ? build_generated_surface_form_fields_html_runtime(state_root, fixture_root, surface_root, context_root, empty_item_root) : NULL;
17141 title = native_json_obj_get_string_dup(native_json_obj_get(surface_root, "admin"), "list_title");
17142 if (title == NULL || title[0] == '\0') {
17143 free(title);
17144 title = native_json_obj_get_string_dup(surface_root, "surface_id");
17145 }
17146 if (title == NULL || title[0] == '\0') {
17147 free(title);
17148 title = strdup("Generated Admin");
17149 }
17150 intro = native_json_obj_get_string_dup(native_json_obj_get(surface_root, "admin"), "list_intro");
17151 if (intro == NULL || intro[0] == '\0') { free(intro); intro = strdup("Framework-owned generated admin surface."); }
17152 filter_title = native_json_obj_get_string_dup(native_json_obj_get(surface_root, "admin"), "filter_title");
17153 if (filter_title == NULL || filter_title[0] == '\0') { free(filter_title); filter_title = strdup("Filter"); }
17154 records_title = native_json_obj_get_string_dup(native_json_obj_get(surface_root, "admin"), "records_title");
17155 if (records_title == NULL || records_title[0] == '\0') { free(records_title); records_title = strdup("Records"); }
17156 create_title = native_json_obj_get_string_dup(native_json_obj_get(surface_root, "admin"), "create_title");
17157 if (create_title == NULL || create_title[0] == '\0') { free(create_title); create_title = strdup("Create"); }
17158 create_button_label = native_json_obj_get_string_dup(native_json_obj_get(surface_root, "admin"), "create_button_label");
17159 if (create_button_label == NULL || create_button_label[0] == '\0') { free(create_button_label); create_button_label = strdup("Create"); }
17160 filter_controls_html = build_generated_surface_filter_controls_html_runtime(surface_root, raw_query != NULL ? raw_query : "");
17161 sort_options_html = build_generated_surface_sort_options_html_runtime(surface_root, raw_query != NULL ? raw_query : "");
17162 if (page_size_text != NULL && page_size_text[0] != '\0') {
17163 current_page_size = strdup(page_size_text);
17164 } else {
17165 char tmp[32];
17166 snprintf(tmp, sizeof(tmp), "%ld", page_default == 0 ? 25 : page_default);
17167 current_page_size = strdup(tmp);
17168 }
17169 {
17170 char tmp[128];
17171 snprintf(tmp, sizeof(tmp), "Page %ld of %ld · %ld total", page_value, page_size_value > 0 ? (long)((total_items + (size_t)page_size_value - 1) / (size_t)page_size_value) : 1, (long)total_items);
17172 pagination_summary = strdup(tmp);
17173 }
17174 if (form_fields_html == NULL || filter_controls_html == NULL || sort_options_html == NULL || current_page_size == NULL || pagination_summary == NULL || header_html == NULL || row_html == NULL) {
17175 goto generated_surface_manage_page_cleanup;
17176 }
17177 yyjson_mut_doc_set_root(out_doc, out_root);
17178 yyjson_mut_obj_add_strcpy(out_doc, out_root, "title", title != NULL ? title : "Generated Admin");
17179 yyjson_mut_obj_add_strcpy(out_doc, out_root, "intro", intro != NULL ? intro : "");
17180 yyjson_mut_obj_add_strcpy(out_doc, out_root, "back_path", back_path != NULL ? back_path : "");
17181 yyjson_mut_obj_add_strcpy(out_doc, out_root, "filter_title", filter_title != NULL ? filter_title : "Filter");
17182 yyjson_mut_obj_add_strcpy(out_doc, out_root, "manage_path", manage_path != NULL ? manage_path : "");
17183 yyjson_mut_obj_add_strcpy(out_doc, out_root, "filter_controls_html", filter_controls_html != NULL ? filter_controls_html : "");
17184 yyjson_mut_obj_add_strcpy(out_doc, out_root, "sort_options_html", sort_options_html != NULL ? sort_options_html : "");
17185 yyjson_mut_obj_add_strcpy(out_doc, out_root, "current_page_size", current_page_size != NULL ? current_page_size : "25");
17186 yyjson_mut_obj_add_strcpy(out_doc, out_root, "pagination_summary", pagination_summary != NULL ? pagination_summary : "");
17187 yyjson_mut_obj_add_strcpy(out_doc, out_root, "records_title", records_title != NULL ? records_title : "Records");
17188 yyjson_mut_obj_add_strcpy(out_doc, out_root, "header_html", header_html != NULL ? header_html : "");
17189 yyjson_mut_obj_add_strcpy(out_doc, out_root, "row_html", row_html != NULL ? row_html : "");
17190 yyjson_mut_obj_add_strcpy(out_doc, out_root, "create_title", create_title != NULL ? create_title : "Create");
17191 yyjson_mut_obj_add_strcpy(out_doc, out_root, "create_path", create_path != NULL ? create_path : "");
17192 yyjson_mut_obj_add_strcpy(out_doc, out_root, "form_fields_html", form_fields_html != NULL ? form_fields_html : "");
17193 yyjson_mut_obj_add_strcpy(out_doc, out_root, "create_button_label", create_button_label != NULL ? create_button_label : "Create");
17194 code = emit_compact_json_mut_value_runtime(out_root);
17195
17196generated_surface_manage_page_cleanup:
17197 free(collection_name);
17198 free(scope_field);
17199 free(sort_key);
17200 free(manage_path);
17201 free(create_path);
17202 free(back_path_template);
17203 free(back_path);
17204 free(manage_table_json);
17205 native_json_doc_free(manage_table_doc);
17206 free(header_html);
17207 free(row_html);
17208 free(form_fields_html);
17209 free(title);
17210 free(intro);
17211 free(filter_title);
17212 free(records_title);
17213 free(create_title);
17214 free(create_button_label);
17215 free(filter_controls_html);
17216 free(sort_options_html);
17217 free(current_page_size);
17218 free(page_size_text);
17219 free(pagination_summary);
17221 native_json_doc_free(state_doc);
17222 native_json_doc_free(fixture_doc);
17223 native_json_doc_free(surface_doc);
17224 native_json_doc_free(context_doc);
17225 native_json_doc_free(empty_item_doc);
17226 yyjson_mut_doc_free(out_doc);
17227 return code;
17228}
17229
17230static int handle_generated_surface_edit_page_json_command(int argc, char **argv) {
17231 const char *state_path = json_option_value_runtime(argc, argv, "--state");
17232 const char *fixture_path = json_option_value_runtime(argc, argv, "--fixture");
17233 const char *surface_json = json_option_value_runtime(argc, argv, "--surface-json");
17234 const char *context_json = json_option_value_runtime(argc, argv, "--context-json");
17235 const char *item_json = json_option_value_runtime(argc, argv, "--item-json");
17236 yyjson_doc *state_doc = NULL;
17237 yyjson_doc *fixture_doc = NULL;
17238 yyjson_doc *surface_doc = NULL;
17239 yyjson_doc *context_doc = NULL;
17240 yyjson_doc *item_doc = NULL;
17241 yyjson_val *state_root = NULL;
17242 yyjson_val *fixture_root = NULL;
17243 yyjson_val *surface_root = NULL;
17244 yyjson_val *context_root = NULL;
17245 yyjson_val *item_root = NULL;
17246 yyjson_mut_doc *out_doc = NULL;
17247 yyjson_mut_val *out_root = NULL;
17248 char *identity_field = NULL;
17249 char *item_id = NULL;
17250 char *update_path = NULL;
17251 char *delete_path = NULL;
17252 char *manage_path = NULL;
17253 char *form_fields_html = NULL;
17254 char *save_button_label = NULL;
17255 char *delete_button_label = NULL;
17256 char *title_field = NULL;
17257 char *title_value = NULL;
17258 char *title = NULL;
17259 char *delete_form_html = NULL;
17260 int code = 69;
17261 if (state_path == NULL || fixture_path == NULL || surface_json == NULL || context_json == NULL) {
17262 return 64;
17263 }
17264 state_doc = native_json_doc_load_file(state_path);
17265 fixture_doc = native_json_doc_load_file(fixture_path);
17266 surface_root = load_root_from_text_runtime(surface_json, &surface_doc);
17267 context_root = load_root_from_text_runtime(context_json, &context_doc);
17268 item_root = load_root_from_text_runtime(item_json != NULL ? item_json : "{}", &item_doc);
17269 state_root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
17270 fixture_root = fixture_doc != NULL ? yyjson_doc_get_root(fixture_doc) : NULL;
17271 if (state_root == NULL || fixture_root == NULL || surface_root == NULL || context_root == NULL || item_root == NULL || !yyjson_is_obj(state_root) || !yyjson_is_obj(fixture_root) || !yyjson_is_obj(surface_root) || !yyjson_is_obj(context_root) || !yyjson_is_obj(item_root)) {
17272 code = 64;
17273 goto generated_surface_edit_page_cleanup;
17274 }
17275 identity_field = native_json_obj_get_string_dup(surface_root, "identity_field");
17276 if (identity_field == NULL || identity_field[0] == '\0') { free(identity_field); identity_field = strdup("id"); }
17277 item_id = json_value_tostring_dup_runtime(native_json_obj_get(item_root, identity_field));
17278 update_path = generated_surface_operation_path_dup_runtime(surface_root, context_root, "update", item_id);
17279 delete_path = generated_surface_operation_path_dup_runtime(surface_root, context_root, "delete", item_id);
17280 manage_path = generated_surface_operation_path_dup_runtime(surface_root, context_root, "manage", NULL);
17281 form_fields_html = build_generated_surface_form_fields_html_runtime(state_root, fixture_root, surface_root, context_root, item_root);
17282 save_button_label = native_json_obj_get_string_dup(native_json_obj_get(surface_root, "admin"), "save_button_label");
17283 if (save_button_label == NULL || save_button_label[0] == '\0') { free(save_button_label); save_button_label = strdup("Save"); }
17284 delete_button_label = generated_surface_delete_button_label_dup_runtime(surface_root);
17285 title_field = native_json_obj_get_string_dup(native_json_obj_get(surface_root, "admin"), "item_title_field");
17286 if (title_field == NULL || title_field[0] == '\0') { free(title_field); title_field = strdup("name"); }
17287 title_value = generated_surface_display_value_dup_runtime(item_root, title_field);
17288 if (title_value == NULL || title_value[0] == '\0') {
17289 free(title_value);
17290 {
17291 char tmp[64];
17292 snprintf(tmp, sizeof(tmp), "Record %s", item_id != NULL ? item_id : "");
17293 title_value = strdup(tmp);
17294 }
17295 }
17296 {
17297 size_t needed = strlen("Edit ") + strlen(title_value != NULL ? title_value : "") + 1;
17298 title = (char *)malloc(needed);
17299 if (title != NULL) {
17300 snprintf(title, needed, "Edit %s", title_value != NULL ? title_value : "");
17301 }
17302 }
17303 if (delete_path != NULL && delete_path[0] != '\0') {
17304 TextBufferRuntime buffer = {0};
17305 if (!text_buffer_append_text_runtime(&buffer, "<form method=\"post\" action=\"")
17306 || !text_buffer_append_html_escaped_runtime(&buffer, delete_path)
17307 || !text_buffer_append_text_runtime(&buffer, "\" style=\"margin-top:1rem;\"><button type=\"submit\" class=\"btn btn-danger\">")
17308 || !text_buffer_append_html_escaped_runtime(&buffer, delete_button_label != NULL ? delete_button_label : "Delete")
17309 || !text_buffer_append_text_runtime(&buffer, "</button></form>")) {
17310 text_buffer_free_runtime(&buffer);
17311 goto generated_surface_edit_page_cleanup;
17312 }
17313 delete_form_html = text_buffer_take_runtime(&buffer);
17314 }
17315 out_doc = yyjson_mut_doc_new(NULL);
17316 out_root = out_doc != NULL ? yyjson_mut_obj(out_doc) : NULL;
17317 if (out_doc == NULL || out_root == NULL || form_fields_html == NULL || title == NULL) {
17318 goto generated_surface_edit_page_cleanup;
17319 }
17320 yyjson_mut_doc_set_root(out_doc, out_root);
17321 yyjson_mut_obj_add_strcpy(out_doc, out_root, "title", title != NULL ? title : "Edit Record");
17322 yyjson_mut_obj_add_strcpy(out_doc, out_root, "manage_path", manage_path != NULL ? manage_path : "");
17323 yyjson_mut_obj_add_strcpy(out_doc, out_root, "update_path", update_path != NULL ? update_path : "");
17324 yyjson_mut_obj_add_strcpy(out_doc, out_root, "form_fields_html", form_fields_html != NULL ? form_fields_html : "");
17325 yyjson_mut_obj_add_strcpy(out_doc, out_root, "save_button_label", save_button_label != NULL ? save_button_label : "Save");
17326 yyjson_mut_obj_add_strcpy(out_doc, out_root, "delete_form_html", delete_form_html != NULL ? delete_form_html : "");
17327 code = emit_compact_json_mut_value_runtime(out_root);
17328
17329generated_surface_edit_page_cleanup:
17330 free(identity_field);
17331 free(item_id);
17332 free(update_path);
17333 free(delete_path);
17334 free(manage_path);
17335 free(form_fields_html);
17336 free(save_button_label);
17337 free(delete_button_label);
17338 free(title_field);
17339 free(title_value);
17340 free(title);
17341 free(delete_form_html);
17342 native_json_doc_free(state_doc);
17343 native_json_doc_free(fixture_doc);
17344 native_json_doc_free(surface_doc);
17345 native_json_doc_free(context_doc);
17346 native_json_doc_free(item_doc);
17347 yyjson_mut_doc_free(out_doc);
17348 return code;
17349}
17350
17351static int handle_generated_surface_context_command(int argc, char **argv) {
17352 const char *state_path = json_option_value_runtime(argc, argv, "--state");
17353 const char *surface_json = json_option_value_runtime(argc, argv, "--surface-json");
17354 const char *params_json = json_option_value_runtime(argc, argv, "--params-json");
17355 yyjson_doc *state_doc = NULL;
17356 yyjson_doc *surface_doc = NULL;
17357 yyjson_doc *params_doc = NULL;
17358 yyjson_val *state_root = NULL;
17359 yyjson_val *surface_root = NULL;
17360 yyjson_val *params_root = NULL;
17361 yyjson_val *binding = NULL;
17362 yyjson_mut_doc *out_doc = NULL;
17363 yyjson_mut_val *out_root = NULL;
17364 yyjson_val *record = NULL;
17365 yyjson_val *context_id_value = NULL;
17366 char *state_collection = NULL;
17367 char *param_name = NULL;
17368 char *match_field = NULL;
17369 char *context_label_field = NULL;
17370 char *tenant_collection = NULL;
17371 char *tenant_match_field = NULL;
17372 char *tenant_match_from_context_field = NULL;
17373 char *context_value = NULL;
17374 char *context_id_field = NULL;
17375 char *context_label = NULL;
17376 long tenant_id = 0;
17377 int code = 69;
17378 if (state_path == NULL || surface_json == NULL || params_json == NULL) {
17379 return 64;
17380 }
17381 state_doc = native_json_doc_load_file(state_path);
17382 surface_root = load_root_from_text_runtime(surface_json, &surface_doc);
17383 params_root = load_root_from_text_runtime(params_json, &params_doc);
17384 state_root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
17385 if (state_root == NULL || surface_root == NULL || params_root == NULL || !yyjson_is_obj(state_root) || !yyjson_is_obj(surface_root) || !yyjson_is_obj(params_root)) {
17386 native_json_doc_free(state_doc);
17387 native_json_doc_free(surface_doc);
17388 native_json_doc_free(params_doc);
17389 return 64;
17390 }
17391 binding = native_json_obj_get(surface_root, "context_binding");
17392 state_collection = native_json_obj_get_string_dup(binding, "state_collection");
17393 param_name = native_json_obj_get_string_dup(binding, "param");
17394 match_field = native_json_obj_get_string_dup(binding, "match_field");
17395 context_id_field = native_json_obj_get_string_dup(binding, "context_id_field");
17396 context_label_field = native_json_obj_get_string_dup(binding, "context_label_field");
17397 tenant_collection = native_json_obj_get_string_dup(binding, "tenant_collection");
17398 tenant_match_field = native_json_obj_get_string_dup(binding, "tenant_match_field");
17399 tenant_match_from_context_field = native_json_obj_get_string_dup(binding, "tenant_match_from_context_field");
17400 if (context_id_field == NULL || context_id_field[0] == '\0') {
17401 free(context_id_field);
17402 context_id_field = strdup("id");
17403 }
17404 out_doc = yyjson_mut_doc_new(NULL);
17405 out_root = out_doc != NULL ? yyjson_mut_obj(out_doc) : NULL;
17406 if (out_doc == NULL || out_root == NULL) {
17407 goto generated_surface_context_cleanup;
17408 }
17409 if (state_collection == NULL || state_collection[0] == '\0' || param_name == NULL || param_name[0] == '\0' || match_field == NULL || match_field[0] == '\0') {
17410 yyjson_mut_obj_add_bool(out_doc, out_root, "ok", 1);
17411 yyjson_mut_obj_add_int(out_doc, out_root, "tenant_id", 0);
17412 yyjson_mut_obj_add_strcpy(out_doc, out_root, "tenant_scope", "global");
17413 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "context_id"), yyjson_mut_null(out_doc));
17414 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "context_param"), yyjson_mut_null(out_doc));
17415 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "context_value"), yyjson_mut_null(out_doc));
17416 yyjson_mut_obj_add_strcpy(out_doc, out_root, "context_label", "");
17417 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "record"), yyjson_mut_null(out_doc));
17418 yyjson_mut_doc_set_root(out_doc, out_root);
17419 code = emit_compact_json_mut_value_runtime(out_root);
17420 goto generated_surface_context_cleanup;
17421 }
17422 context_value = json_value_tostring_dup_runtime(native_json_obj_get(params_root, param_name));
17423 if (context_value == NULL || context_value[0] == '\0') {
17424 yyjson_mut_obj_add_bool(out_doc, out_root, "ok", 0);
17425 yyjson_mut_obj_add_strcpy(out_doc, out_root, "error_code", "missing_context_parameter");
17426 yyjson_mut_obj_add_strcpy(out_doc, out_root, "message", "Missing route parameter.");
17427 yyjson_mut_doc_set_root(out_doc, out_root);
17428 code = emit_compact_json_mut_value_runtime(out_root);
17429 goto generated_surface_context_cleanup;
17430 }
17431 record = surface_find_record_by_string_field_runtime(state_root, state_collection, match_field, context_value);
17432 if (record == NULL) {
17433 yyjson_mut_obj_add_bool(out_doc, out_root, "ok", 0);
17434 yyjson_mut_obj_add_strcpy(out_doc, out_root, "error_code", "context_not_found");
17435 yyjson_mut_obj_add_strcpy(out_doc, out_root, "message", "Context not found.");
17436 yyjson_mut_doc_set_root(out_doc, out_root);
17437 code = emit_compact_json_mut_value_runtime(out_root);
17438 goto generated_surface_context_cleanup;
17439 }
17440 context_id_value = native_json_obj_get(record, context_id_field);
17441 if (tenant_collection != NULL && tenant_collection[0] != '\0' && tenant_match_field != NULL && tenant_match_field[0] != '\0' && tenant_match_from_context_field != NULL && tenant_match_from_context_field[0] != '\0') {
17442 char *tenant_match_value = json_value_tostring_dup_runtime(native_json_obj_get(record, tenant_match_from_context_field));
17443 yyjson_val *tenant_record = tenant_match_value != NULL && tenant_match_value[0] != '\0' ? surface_find_record_by_string_field_runtime(state_root, tenant_collection, tenant_match_field, tenant_match_value) : NULL;
17444 if (tenant_record != NULL) {
17445 tenant_id = native_json_obj_get_long_default(tenant_record, "id", 0, NULL);
17446 }
17447 free(tenant_match_value);
17448 }
17449 context_label = context_label_field != NULL && context_label_field[0] != '\0' ? native_json_obj_get_string_dup(record, context_label_field) : strdup("");
17450 yyjson_mut_obj_add_bool(out_doc, out_root, "ok", 1);
17451 yyjson_mut_obj_add_int(out_doc, out_root, "tenant_id", tenant_id);
17452 if (tenant_id != 0) {
17453 char tenant_scope_text[64];
17454 snprintf(tenant_scope_text, sizeof(tenant_scope_text), "tenant:%ld", tenant_id);
17455 yyjson_mut_obj_add_strcpy(out_doc, out_root, "tenant_scope", tenant_scope_text);
17456 } else {
17457 yyjson_mut_obj_add_strcpy(out_doc, out_root, "tenant_scope", "global");
17458 }
17459 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "context_id"), context_id_value != NULL ? yyjson_val_mut_copy(out_doc, context_id_value) : yyjson_mut_null(out_doc));
17460 yyjson_mut_obj_add_strcpy(out_doc, out_root, "context_param", param_name);
17461 yyjson_mut_obj_add_strcpy(out_doc, out_root, "context_value", context_value);
17462 yyjson_mut_obj_add_strcpy(out_doc, out_root, "context_label", context_label != NULL ? context_label : "");
17463 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "record"), yyjson_val_mut_copy(out_doc, record));
17464 yyjson_mut_doc_set_root(out_doc, out_root);
17465 code = emit_compact_json_mut_value_runtime(out_root);
17466generated_surface_context_cleanup:
17467 free(state_collection);
17468 free(param_name);
17469 free(match_field);
17470 free(context_id_field);
17471 free(context_label_field);
17472 free(tenant_collection);
17473 free(tenant_match_field);
17474 free(tenant_match_from_context_field);
17475 free(context_value);
17476 free(context_label);
17477 native_json_doc_free(state_doc);
17478 native_json_doc_free(surface_doc);
17479 native_json_doc_free(params_doc);
17480 yyjson_mut_doc_free(out_doc);
17481 return code;
17482}
17483
17484static int handle_generated_surface_coerce_candidate_command(int argc, char **argv) {
17485 const char *surface_json = json_option_value_runtime(argc, argv, "--surface-json");
17486 const char *candidate_json = json_option_value_runtime(argc, argv, "--candidate-json");
17487 yyjson_doc *surface_doc = NULL;
17488 yyjson_doc *candidate_doc = NULL;
17489 yyjson_val *surface_root = load_root_from_text_runtime(surface_json, &surface_doc);
17490 yyjson_val *candidate_root = load_root_from_text_runtime(candidate_json, &candidate_doc);
17491 yyjson_val *field_types = NULL;
17492 yyjson_obj_iter iter;
17493 yyjson_val *key = NULL;
17494 yyjson_mut_doc *mut_doc = NULL;
17495 yyjson_mut_val *mut_root = NULL;
17496 int code = 69;
17497 if (surface_root == NULL || candidate_root == NULL || !yyjson_is_obj(surface_root) || !yyjson_is_obj(candidate_root)) {
17498 native_json_doc_free(surface_doc);
17499 native_json_doc_free(candidate_doc);
17500 return 64;
17501 }
17502 mut_doc = yyjson_doc_mut_copy(candidate_doc, NULL);
17503 mut_root = mut_doc != NULL ? yyjson_mut_doc_get_root(mut_doc) : NULL;
17504 field_types = native_json_obj_get(surface_root, "field_types");
17505 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root) || field_types == NULL || !yyjson_is_obj(field_types)) {
17506 code = emit_compact_json_mut_value_runtime(mut_root != NULL ? (yyjson_mut_val *)mut_root : yyjson_mut_obj(mut_doc));
17507 native_json_doc_free(surface_doc);
17508 native_json_doc_free(candidate_doc);
17509 yyjson_mut_doc_free(mut_doc);
17510 return code;
17511 }
17512 if (yyjson_obj_iter_init(field_types, &iter)) {
17513 while ((key = yyjson_obj_iter_next(&iter)) != NULL) {
17514 yyjson_val *type_value = yyjson_obj_iter_get_val(key);
17515 const char *field_name = yyjson_get_str(key);
17516 yyjson_mut_val *candidate_value = yyjson_mut_obj_get(mut_root, field_name);
17517 char *type_text = json_value_tostring_dup_runtime(type_value);
17518 if (candidate_value == NULL || type_text == NULL) {
17519 free(type_text);
17520 continue;
17521 }
17522 if (streq(type_text, "integer")) {
17523 char *text = json_value_tostring_dup_runtime((yyjson_val *)candidate_value);
17524 if (text != NULL && text[0] != '\0' && digits_only_text_runtime(text)) {
17525 yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, field_name), yyjson_mut_int(mut_doc, strtol(text, NULL, 10)));
17526 }
17527 free(text);
17528 } else if (streq(type_text, "boolean")) {
17529 if (!yyjson_is_bool((yyjson_val *)candidate_value)) {
17530 yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, field_name), yyjson_mut_bool(mut_doc, json_value_truthy_runtime((yyjson_val *)candidate_value)));
17531 }
17532 } else if (streq(type_text, "array_integer") && yyjson_mut_is_arr(candidate_value)) {
17533 yyjson_mut_val *new_array = yyjson_mut_arr(mut_doc);
17534 size_t idx = 0;
17535 size_t max = 0;
17536 yyjson_mut_val *entry = NULL;
17537 if (new_array != NULL) {
17538 yyjson_mut_arr_foreach(candidate_value, idx, max, entry) {
17539 char *text = json_value_tostring_dup_runtime((yyjson_val *)entry);
17540 if (text != NULL && digits_only_text_runtime(text)) {
17541 yyjson_mut_arr_append(new_array, yyjson_mut_int(mut_doc, strtol(text, NULL, 10)));
17542 } else if (entry != NULL) {
17543 yyjson_mut_arr_append(new_array, yyjson_val_mut_copy(mut_doc, (yyjson_val *)entry));
17544 }
17545 free(text);
17546 }
17547 yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, field_name), new_array);
17548 }
17549 }
17550 free(type_text);
17551 }
17552 }
17553 yyjson_mut_doc_set_root(mut_doc, mut_root);
17554 code = emit_compact_json_mut_value_runtime(mut_root);
17555 native_json_doc_free(surface_doc);
17556 native_json_doc_free(candidate_doc);
17557 yyjson_mut_doc_free(mut_doc);
17558 return code;
17559}
17560
17561static int handle_generated_surface_submitted_command(int argc, char **argv) {
17562 const char *surface_json = json_option_value_runtime(argc, argv, "--surface-json");
17563 const char *form_fixture = json_option_value_runtime(argc, argv, "--form-fixture");
17564 const char *content_type = json_option_value_runtime(argc, argv, "--content-type");
17565 const char *form_body = json_option_value_runtime(argc, argv, "--form-body");
17566 const char *request_body_path = json_option_value_runtime(argc, argv, "--request-body-path");
17567 yyjson_doc *surface_doc = NULL;
17568 yyjson_doc *fixture_doc = NULL;
17569 yyjson_doc *payload_doc = NULL;
17570 yyjson_val *surface_root = load_root_from_text_runtime(surface_json, &surface_doc);
17571 yyjson_val *fixture_root = (form_fixture != NULL && form_fixture[0] != '\0') ? yyjson_doc_get_root((fixture_doc = native_json_doc_load_file(form_fixture))) : NULL;
17572 yyjson_mut_doc *out_doc = NULL;
17573 yyjson_mut_val *out_root = NULL;
17574 char *form_id = NULL;
17575 int code = 69;
17576 if (surface_root == NULL || !yyjson_is_obj(surface_root)) {
17577 native_json_doc_free(surface_doc);
17578 native_json_doc_free(fixture_doc);
17579 return 64;
17580 }
17581 out_doc = yyjson_mut_doc_new(NULL);
17582 out_root = out_doc != NULL ? yyjson_mut_obj(out_doc) : NULL;
17583 form_id = native_json_obj_get_string_dup(surface_root, "form_id");
17584 if (out_doc == NULL || out_root == NULL) {
17585 goto generated_surface_submitted_cleanup;
17586 }
17587 if (content_type != NULL && strncasecmp(content_type, "application/json", 16) == 0) {
17588 char *body_text = NULL;
17589 yyjson_val *payload_root = NULL;
17590 body_text = (request_body_path != NULL && request_body_path[0] != '\0') ? read_file_text(request_body_path) : strdup(form_body != NULL ? form_body : "{}");
17591 payload_root = load_root_from_text_runtime(body_text != NULL ? body_text : "{}", &payload_doc);
17592 if (payload_root != NULL && yyjson_is_obj(payload_root)) {
17593 yyjson_val *form = surface_find_form_runtime(fixture_root, form_id);
17594 yyjson_val *fields = form != NULL ? native_json_obj_get_array(form, "fields") : NULL;
17595 yyjson_val *relation_inputs = native_json_obj_get_array(surface_root, "relation_inputs");
17596 yyjson_arr_iter iter;
17597 yyjson_val *item = NULL;
17598 if (fields != NULL && yyjson_arr_iter_init(fields, &iter)) {
17599 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
17600 char *field_id = native_json_obj_get_string_dup(item, "field_id");
17601 yyjson_val *value = field_id != NULL ? native_json_obj_get(payload_root, field_id) : NULL;
17602 if (field_id != NULL && value != NULL) {
17603 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, field_id), yyjson_val_mut_copy(out_doc, value));
17604 }
17605 free(field_id);
17606 }
17607 }
17608 if (relation_inputs != NULL && yyjson_arr_iter_init(relation_inputs, &iter)) {
17609 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
17610 char *field_name = native_json_obj_get_string_dup(item, "field");
17611 yyjson_val *value = field_name != NULL ? native_json_obj_get(payload_root, field_name) : NULL;
17612 if (field_name != NULL && value != NULL) {
17613 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, field_name), yyjson_val_mut_copy(out_doc, value));
17614 }
17615 free(field_name);
17616 }
17617 }
17618 }
17619 free(body_text);
17620 native_json_doc_free(payload_doc);
17621 payload_doc = NULL;
17622 } else {
17623 yyjson_val *form = surface_find_form_runtime(fixture_root, form_id);
17624 yyjson_val *fields = form != NULL ? native_json_obj_get_array(form, "fields") : NULL;
17625 yyjson_val *relation_inputs = native_json_obj_get_array(surface_root, "relation_inputs");
17626 yyjson_arr_iter iter;
17627 yyjson_val *item = NULL;
17628 if (fields != NULL && yyjson_arr_iter_init(fields, &iter)) {
17629 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
17630 char *field_id = native_json_obj_get_string_dup(item, "field_id");
17631 char *value = field_id != NULL ? query_value_decoded_dup_runtime(form_body != NULL ? form_body : "", field_id) : NULL;
17632 if (field_id != NULL && value != NULL) {
17633 yyjson_mut_obj_add_strcpy(out_doc, out_root, field_id, value);
17634 }
17635 free(field_id);
17636 free(value);
17637 }
17638 }
17639 if (relation_inputs != NULL && yyjson_arr_iter_init(relation_inputs, &iter)) {
17640 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
17641 char *field_name = native_json_obj_get_string_dup(item, "field");
17642 char *checkbox_prefix = native_json_obj_get_string_dup(item, "form_checkbox_prefix");
17643 yyjson_mut_val *array = yyjson_mut_arr(out_doc);
17644 char *body_copy = strdup(form_body != NULL ? form_body : "");
17645 char *part = NULL;
17646 char *saveptr = NULL;
17647 if (field_name != NULL && (checkbox_prefix == NULL || checkbox_prefix[0] == '\0')) {
17648 size_t needed = strlen(field_name) + 3;
17649 free(checkbox_prefix);
17650 checkbox_prefix = (char *)malloc(needed);
17651 if (checkbox_prefix != NULL) {
17652 snprintf(checkbox_prefix, needed, "%s__", field_name);
17653 }
17654 }
17655 if (array != NULL && body_copy != NULL && checkbox_prefix != NULL) {
17656 part = strtok_r(body_copy, "&", &saveptr);
17657 while (part != NULL) {
17658 char *eq = strchr(part, '=');
17659 if (eq != NULL && strncmp(part, checkbox_prefix, strlen(checkbox_prefix)) == 0) {
17660 char *decoded = url_decode_dup_runtime(eq + 1);
17661 if (decoded != NULL && digits_only_text_runtime(decoded)) {
17662 yyjson_mut_arr_append(array, yyjson_mut_int(out_doc, strtol(decoded, NULL, 10)));
17663 }
17664 free(decoded);
17665 }
17666 part = strtok_r(NULL, "&", &saveptr);
17667 }
17668 if (field_name != NULL) {
17669 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, field_name), array);
17670 }
17671 }
17672 free(field_name);
17673 free(checkbox_prefix);
17674 free(body_copy);
17675 }
17676 }
17677 }
17678 yyjson_mut_doc_set_root(out_doc, out_root);
17679 code = emit_compact_json_mut_value_runtime(out_root);
17680generated_surface_submitted_cleanup:
17681 free(form_id);
17682 native_json_doc_free(surface_doc);
17683 native_json_doc_free(fixture_doc);
17684 native_json_doc_free(payload_doc);
17685 yyjson_mut_doc_free(out_doc);
17686 return code;
17687}
17688
17689static int handle_generated_surface_list_result_command(int argc, char **argv) {
17690 const char *state_path = json_option_value_runtime(argc, argv, "--state");
17691 const char *surface_json = json_option_value_runtime(argc, argv, "--surface-json");
17692 const char *context_json = json_option_value_runtime(argc, argv, "--context-json");
17693 const char *raw_query = json_option_value_runtime(argc, argv, "--query");
17694 yyjson_doc *state_doc = NULL;
17695 yyjson_doc *surface_doc = NULL;
17696 yyjson_doc *context_doc = NULL;
17697 yyjson_val *state_root = NULL;
17698 yyjson_val *surface_root = NULL;
17699 yyjson_val *context_root = NULL;
17700 yyjson_val *items_array = NULL;
17701 yyjson_val *context_id_value = NULL;
17702 json_value_list_runtime filtered = {0};
17703 yyjson_arr_iter iter;
17704 yyjson_val *item = NULL;
17705 yyjson_mut_doc *out_doc = NULL;
17706 yyjson_mut_val *out_root = NULL;
17707 yyjson_mut_val *items_out = NULL;
17708 yyjson_mut_val *pagination = NULL;
17709 yyjson_mut_val *filters = NULL;
17710 yyjson_mut_val *sort_config = NULL;
17711 char *collection_name = NULL;
17712 char *scope_field = NULL;
17713 char *sort_key = NULL;
17714 long page_default = 25;
17715 long page_max = 100;
17716 long page_value = 1;
17717 long page_size_value = 25;
17718 size_t total_items = 0;
17719 size_t start_index = 0;
17720 size_t end_index = 0;
17721 int code = 69;
17722 if (state_path == NULL || surface_json == NULL || context_json == NULL) {
17723 return 64;
17724 }
17725 state_doc = native_json_doc_load_file(state_path);
17726 surface_root = load_root_from_text_runtime(surface_json, &surface_doc);
17727 context_root = load_root_from_text_runtime(context_json, &context_doc);
17728 state_root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
17729 if (state_root == NULL || surface_root == NULL || context_root == NULL || !yyjson_is_obj(state_root) || !yyjson_is_obj(surface_root) || !yyjson_is_obj(context_root)) {
17730 code = 64;
17731 goto generated_surface_list_cleanup;
17732 }
17733 collection_name = native_json_obj_get_string_dup(surface_root, "entity_collection");
17734 scope_field = native_json_obj_get_string_dup(surface_root, "scope_field");
17735 context_id_value = native_json_obj_get(context_root, "context_id");
17736 items_array = collection_name != NULL ? state_array_runtime(state_root, collection_name) : NULL;
17737 if (items_array == NULL || !yyjson_arr_iter_init(items_array, &iter)) {
17738 items_array = NULL;
17739 } else {
17740 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
17741 if (!yyjson_is_obj(item)) {
17742 continue;
17743 }
17744 if (!surface_context_match_runtime(item, scope_field, context_id_value)) {
17745 continue;
17746 }
17747 if (!json_value_list_append_runtime(&filtered, item)) {
17748 code = 69;
17749 goto generated_surface_list_cleanup;
17750 }
17751 }
17752 }
17753 out_doc = yyjson_mut_doc_new(NULL);
17754 out_root = out_doc != NULL ? yyjson_mut_obj(out_doc) : NULL;
17755 items_out = out_doc != NULL ? yyjson_mut_arr(out_doc) : NULL;
17756 pagination = out_doc != NULL ? yyjson_mut_obj(out_doc) : NULL;
17757 filters = out_doc != NULL ? surface_build_active_filters_runtime(out_doc, surface_root, raw_query != NULL ? raw_query : "") : NULL;
17758 sort_config = out_doc != NULL ? surface_build_sort_config_runtime(out_doc, surface_root, raw_query != NULL ? raw_query : "") : NULL;
17759 if (out_doc == NULL || out_root == NULL || items_out == NULL || pagination == NULL || filters == NULL || sort_config == NULL) {
17760 code = 69;
17761 goto generated_surface_list_cleanup;
17762 }
17763 page_default = native_json_obj_get_long_default(surface_root, "page_size_default", 25, NULL);
17764 page_max = native_json_obj_get_long_default(surface_root, "page_size_max", 100, NULL);
17765 {
17766 char *page_text = query_value_decoded_dup_runtime(raw_query != NULL ? raw_query : "", "page");
17767 char *page_size_text = query_value_decoded_dup_runtime(raw_query != NULL ? raw_query : "", "page_size");
17768 sort_key = query_value_decoded_dup_runtime(raw_query != NULL ? raw_query : "", "sort");
17769 if (page_text != NULL && digits_only_text_runtime(page_text) && strtol(page_text, NULL, 10) > 0) {
17770 page_value = strtol(page_text, NULL, 10);
17771 }
17772 if (page_size_text != NULL && digits_only_text_runtime(page_size_text) && strtol(page_size_text, NULL, 10) > 0) {
17773 page_size_value = strtol(page_size_text, NULL, 10);
17774 } else {
17775 page_size_value = page_default;
17776 }
17777 if (page_size_value > page_max) {
17778 page_size_value = page_max;
17779 }
17780 free(page_text);
17781 free(page_size_text);
17782 }
17783 {
17784 json_value_list_runtime visible = {0};
17785 size_t idx = 0;
17786 for (idx = 0; idx < filtered.count; idx++) {
17787 if (surface_filter_item_runtime(filtered.items[idx], (yyjson_val *)filters) && json_value_list_append_runtime(&visible, filtered.items[idx])) {
17788 }
17789 }
17791 filtered = visible;
17792 }
17793 surface_sort_items_runtime(&filtered, (yyjson_val *)sort_config);
17794 total_items = filtered.count;
17795 start_index = (size_t)((page_value - 1) * page_size_value);
17796 if (start_index > total_items) {
17797 start_index = total_items;
17798 }
17799 end_index = start_index + (size_t)page_size_value;
17800 if (end_index > total_items) {
17801 end_index = total_items;
17802 }
17803 {
17804 yyjson_val *response_fields = native_json_obj_get_array(surface_root, "response_fields");
17805 size_t idx = 0;
17806 for (idx = start_index; idx < end_index; idx++) {
17807 yyjson_mut_val *entry = NULL;
17808 if (response_fields != NULL && yyjson_is_arr(response_fields) && yyjson_arr_size(response_fields) > 0) {
17809 yyjson_arr_iter field_iter;
17810 yyjson_val *field = NULL;
17811 entry = yyjson_mut_obj(out_doc);
17812 if (entry != NULL && yyjson_arr_iter_init(response_fields, &field_iter)) {
17813 while ((field = yyjson_arr_iter_next(&field_iter)) != NULL) {
17814 if (yyjson_is_str(field)) {
17815 const char *field_name = yyjson_get_str(field);
17816 yyjson_val *value = native_json_obj_get(filtered.items[idx], field_name);
17817 yyjson_mut_obj_put(entry, yyjson_mut_strcpy(out_doc, field_name), value != NULL ? yyjson_val_mut_copy(out_doc, value) : yyjson_mut_null(out_doc));
17818 }
17819 }
17820 }
17821 } else {
17822 entry = yyjson_val_mut_copy(out_doc, filtered.items[idx]);
17823 }
17824 if (entry != NULL) {
17825 yyjson_mut_arr_append(items_out, entry);
17826 }
17827 }
17828 }
17829 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "items"), items_out);
17830 yyjson_mut_obj_add_int(out_doc, pagination, "page", page_value);
17831 yyjson_mut_obj_add_int(out_doc, pagination, "page_size", page_size_value);
17832 yyjson_mut_obj_add_int(out_doc, pagination, "total_items", (long)total_items);
17833 yyjson_mut_obj_add_int(out_doc, pagination, "total_pages", page_size_value > 0 ? (long)((total_items + (size_t)page_size_value - 1) / (size_t)page_size_value) : 1);
17834 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "pagination"), pagination);
17835 yyjson_mut_obj_put(out_root, yyjson_mut_strcpy(out_doc, "sort"), (sort_key != NULL && sort_key[0] != '\0') ? yyjson_mut_strcpy(out_doc, sort_key) : yyjson_mut_null(out_doc));
17836 yyjson_mut_doc_set_root(out_doc, out_root);
17837 code = emit_compact_json_mut_value_runtime(out_root);
17838generated_surface_list_cleanup:
17839 free(collection_name);
17840 free(scope_field);
17841 free(sort_key);
17843 native_json_doc_free(state_doc);
17844 native_json_doc_free(surface_doc);
17845 native_json_doc_free(context_doc);
17846 yyjson_mut_doc_free(out_doc);
17847 return code;
17848}
17849
17851 const char *state_path = json_option_value_runtime(argc, argv, "--state");
17852 const char *surface_json = json_option_value_runtime(argc, argv, "--surface-json");
17853 const char *context_json = json_option_value_runtime(argc, argv, "--context-json");
17854 const char *existing_item_json = json_option_value_runtime(argc, argv, "--existing-item-json");
17855 const char *submitted_json = json_option_value_runtime(argc, argv, "--submitted-json");
17856 yyjson_doc *state_doc = NULL;
17857 yyjson_doc *surface_doc = NULL;
17858 yyjson_doc *context_doc = NULL;
17859 yyjson_doc *existing_doc = NULL;
17860 yyjson_doc *submitted_doc = NULL;
17861 yyjson_val *state_root = NULL;
17862 yyjson_val *surface_root = NULL;
17863 yyjson_val *context_root = NULL;
17864 yyjson_val *existing_root = load_root_from_text_runtime(existing_item_json != NULL && existing_item_json[0] != '\0' ? existing_item_json : "{}", &existing_doc);
17865 yyjson_val *submitted_root = load_root_from_text_runtime(submitted_json != NULL && submitted_json[0] != '\0' ? submitted_json : "{}", &submitted_doc);
17866 yyjson_mut_doc *mut_doc = NULL;
17867 yyjson_mut_val *mut_root = NULL;
17868 char *scope_field = NULL;
17869 char *auto_sequence_field = NULL;
17870 char *entity_collection = NULL;
17871 int code = 69;
17872 if (state_path == NULL || surface_json == NULL || context_json == NULL) {
17873 return 64;
17874 }
17875 state_doc = native_json_doc_load_file(state_path);
17876 surface_root = load_root_from_text_runtime(surface_json, &surface_doc);
17877 context_root = load_root_from_text_runtime(context_json, &context_doc);
17878 state_root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
17879 if (state_root == NULL || surface_root == NULL || context_root == NULL || existing_root == NULL || submitted_root == NULL || !yyjson_is_obj(state_root) || !yyjson_is_obj(surface_root) || !yyjson_is_obj(context_root) || !yyjson_is_obj(existing_root) || !yyjson_is_obj(submitted_root)) {
17880 code = 64;
17881 goto generated_surface_default_cleanup;
17882 }
17883 mut_doc = yyjson_mut_doc_new(NULL);
17884 mut_root = yyjson_mut_obj(mut_doc);
17885 if (mut_doc == NULL || mut_root == NULL) {
17886 code = 69;
17887 goto generated_surface_default_cleanup;
17888 }
17889 yyjson_mut_doc_set_root(mut_doc, mut_root);
17890 merge_patch_into_mut_object_runtime(mut_doc, mut_root, existing_root);
17891 merge_patch_into_mut_object_runtime(mut_doc, mut_root, native_json_obj_get(surface_root, "defaults"));
17892 merge_patch_into_mut_object_runtime(mut_doc, mut_root, submitted_root);
17893 scope_field = native_json_obj_get_string_dup(surface_root, "scope_field");
17894 if (scope_field != NULL && scope_field[0] != '\0') {
17895 yyjson_val *context_id = native_json_obj_get(context_root, "context_id");
17896 if (context_id != NULL && !yyjson_is_null(context_id)) {
17897 yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, scope_field), yyjson_val_mut_copy(mut_doc, context_id));
17898 }
17899 }
17900 auto_sequence_field = native_json_obj_get_string_dup(surface_root, "auto_sequence_field");
17901 entity_collection = native_json_obj_get_string_dup(surface_root, "entity_collection");
17902 if ((existing_item_json == NULL || existing_item_json[0] == '\0') && auto_sequence_field != NULL && auto_sequence_field[0] != '\0' && entity_collection != NULL && entity_collection[0] != '\0') {
17903 yyjson_mut_val *current_value = yyjson_mut_obj_get(mut_root, auto_sequence_field);
17904 char *current_text = json_value_tostring_dup_runtime((yyjson_val *)current_value);
17905 if (current_text == NULL || current_text[0] == '\0') {
17906 yyjson_val *rows = state_array_runtime(state_root, entity_collection);
17907 yyjson_arr_iter iter;
17908 yyjson_val *item = NULL;
17909 long next_value = 1;
17910 yyjson_val *context_id = native_json_obj_get(context_root, "context_id");
17911 if (rows != NULL && yyjson_arr_iter_init(rows, &iter)) {
17912 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
17913 long candidate_value = 0;
17914 yyjson_val *field_value = native_json_obj_get(item, auto_sequence_field);
17915 if (!surface_context_match_runtime(item, scope_field, context_id)) {
17916 continue;
17917 }
17918 if (!long_from_value_runtime(field_value, &candidate_value)) {
17919 candidate_value = native_json_obj_get_long_default(item, "id", 0, NULL);
17920 }
17921 if (candidate_value >= next_value) {
17922 next_value = candidate_value + 1;
17923 }
17924 }
17925 }
17926 yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, auto_sequence_field), yyjson_mut_int(mut_doc, next_value));
17927 }
17928 free(current_text);
17929 }
17930 {
17931 char *candidate_text = NULL;
17932 candidate_text = yyjson_mut_val_write(mut_root, YYJSON_WRITE_NOFLAG, NULL);
17933 if (candidate_text != NULL) {
17934 char *argv_local[] = {"--surface-json", (char *)surface_json, "--candidate-json", candidate_text};
17936 free(candidate_text);
17937 goto generated_surface_default_cleanup;
17938 }
17939 }
17940 code = emit_compact_json_mut_value_runtime(mut_root);
17941 generated_surface_default_cleanup:
17942 free(scope_field);
17943 free(auto_sequence_field);
17944 free(entity_collection);
17945 native_json_doc_free(state_doc);
17946 native_json_doc_free(surface_doc);
17947 native_json_doc_free(context_doc);
17948 native_json_doc_free(existing_doc);
17949 native_json_doc_free(submitted_doc);
17950 yyjson_mut_doc_free(mut_doc);
17951 return code;
17952}
17953
17955 const char *state_path = json_option_value_runtime(argc, argv, "--state");
17956 const char *surface_json = json_option_value_runtime(argc, argv, "--surface-json");
17957 const char *context_json = json_option_value_runtime(argc, argv, "--context-json");
17958 const char *candidate_json = json_option_value_runtime(argc, argv, "--candidate-json");
17959 const char *current_id_text = json_option_value_runtime(argc, argv, "--current-id");
17960 const char *report_json = json_option_value_runtime(argc, argv, "--report-json");
17961 yyjson_doc *state_doc = NULL;
17962 yyjson_doc *surface_doc = NULL;
17963 yyjson_doc *context_doc = NULL;
17964 yyjson_doc *candidate_doc = NULL;
17965 yyjson_doc *report_doc = NULL;
17966 yyjson_mut_doc *mut_doc = NULL;
17967 yyjson_val *state_root = NULL;
17968 yyjson_val *surface_root = NULL;
17969 yyjson_val *context_root = NULL;
17970 yyjson_val *candidate_root = NULL;
17971 yyjson_val *report_root = NULL;
17972 yyjson_mut_val *mut_root = NULL;
17973 yyjson_val *rules = NULL;
17974 yyjson_arr_iter iter;
17975 yyjson_val *item = NULL;
17976 long current_id = 0;
17977 int has_current_id = 0;
17978 int code = 69;
17979 if (state_path == NULL || surface_json == NULL || context_json == NULL || candidate_json == NULL || report_json == NULL) {
17980 return 64;
17981 }
17982 state_doc = native_json_doc_load_file(state_path);
17983 surface_root = load_root_from_text_runtime(surface_json, &surface_doc);
17984 context_root = load_root_from_text_runtime(context_json, &context_doc);
17985 candidate_root = load_root_from_text_runtime(candidate_json, &candidate_doc);
17986 report_root = load_root_from_text_runtime(report_json, &report_doc);
17987 state_root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
17988 if (state_root == NULL || surface_root == NULL || context_root == NULL || candidate_root == NULL || report_root == NULL || !yyjson_is_obj(state_root) || !yyjson_is_obj(surface_root) || !yyjson_is_obj(context_root) || !yyjson_is_obj(candidate_root) || !yyjson_is_obj(report_root)) {
17989 code = 64;
17990 goto generated_surface_validation_cleanup;
17991 }
17992 if (current_id_text != NULL && current_id_text[0] != '\0' && digits_only_text_runtime(current_id_text)) {
17993 current_id = strtol(current_id_text, NULL, 10);
17994 has_current_id = 1;
17995 }
17996 mut_doc = yyjson_doc_mut_copy(report_doc, NULL);
17997 mut_root = mut_doc != NULL ? yyjson_mut_doc_get_root(mut_doc) : NULL;
17998 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
17999 code = 69;
18000 goto generated_surface_validation_cleanup;
18001 }
18002 rules = native_json_obj_get_array(surface_root, "field_constraints");
18003 if (rules != NULL && yyjson_arr_iter_init(rules, &iter)) {
18004 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
18005 char *field_name = native_json_obj_get_string_dup(item, "field");
18006 char *constraint_kind = native_json_obj_get_string_dup(item, "kind");
18007 char *rule_id = native_json_obj_get_string_dup(item, "rule_id");
18008 yyjson_val *field_value = field_name != NULL ? native_json_obj_get(candidate_root, field_name) : NULL;
18009 char *field_text = json_value_tostring_dup_runtime(field_value);
18010 char *label = generated_surface_label_from_field_runtime(field_name);
18011 if (rule_id == NULL) {
18012 rule_id = strdup("validation_rule");
18013 }
18014 if (field_name != NULL && constraint_kind != NULL && field_text != NULL && field_text[0] != '\0' && label != NULL) {
18015 char message[512];
18016 if (streq(constraint_kind, "positive_integer")) {
18017 if (!digits_only_text_runtime(field_text) || strtol(field_text, NULL, 10) <= 0) {
18018 snprintf(message, sizeof(message), "%s must be a positive whole number.", label);
18019 generated_surface_report_add_field_error_runtime(mut_doc, mut_root, field_name, rule_id, "invalid_integer", message);
18020 }
18021 } else if (streq(constraint_kind, "decimal")) {
18023 snprintf(message, sizeof(message), "%s must be a valid decimal amount.", label);
18024 generated_surface_report_add_field_error_runtime(mut_doc, mut_root, field_name, rule_id, "invalid_decimal", message);
18025 }
18026 } else if (streq(constraint_kind, "email")) {
18027 if (!generated_surface_text_is_email_runtime(field_text)) {
18028 snprintf(message, sizeof(message), "%s must be a valid email address.", label);
18029 generated_surface_report_add_field_error_runtime(mut_doc, mut_root, field_name, rule_id, "invalid_email", message);
18030 }
18031 } else if (streq(constraint_kind, "enum")) {
18032 yyjson_val *allowed_values = native_json_obj_get_array(item, "allowed_values");
18033 if (!generated_surface_array_contains_string_runtime(allowed_values, field_text)) {
18034 snprintf(message, sizeof(message), "%s has an invalid value.", label);
18035 generated_surface_report_add_field_error_runtime(mut_doc, mut_root, field_name, rule_id, "invalid_choice", message);
18036 }
18037 } else if (streq(constraint_kind, "time")) {
18038 if (!generated_surface_text_is_time_runtime(field_text)) {
18039 snprintf(message, sizeof(message), "%s must be a valid time.", label);
18040 generated_surface_report_add_field_error_runtime(mut_doc, mut_root, field_name, rule_id, "invalid_time", message);
18041 }
18042 } else if (streq(constraint_kind, "date")) {
18043 if (!generated_surface_text_is_date_runtime(field_text)) {
18044 snprintf(message, sizeof(message), "%s must be a valid date.", label);
18045 generated_surface_report_add_field_error_runtime(mut_doc, mut_root, field_name, rule_id, "invalid_date", message);
18046 }
18047 }
18048 }
18049 free(field_name);
18050 free(constraint_kind);
18051 free(rule_id);
18052 free(field_text);
18053 free(label);
18054 }
18055 }
18056 rules = native_json_obj_get_array(surface_root, "relation_inputs");
18057 if (rules != NULL && yyjson_arr_iter_init(rules, &iter)) {
18058 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
18059 char *relation_field = native_json_obj_get_string_dup(item, "field");
18060 char *related_collection = native_json_obj_get_string_dup(item, "related_collection");
18061 char *related_scope_field = native_json_obj_get_string_dup(item, "related_scope_field");
18062 char *invalid_rule_id = native_json_obj_get_string_dup(item, "invalid_rule_id");
18063 char *invalid_scope_rule_id = native_json_obj_get_string_dup(item, "scope_rule_id");
18064 char *invalid_message = native_json_obj_get_string_dup(item, "invalid_message");
18065 char *invalid_scope_message = native_json_obj_get_string_dup(item, "scope_message");
18066 yyjson_val *relation_value = relation_field != NULL ? native_json_obj_get(candidate_root, relation_field) : NULL;
18067 if (invalid_rule_id == NULL) invalid_rule_id = strdup("invalid_relation");
18068 if (invalid_scope_rule_id == NULL) invalid_scope_rule_id = strdup("invalid_relation_scope");
18069 if (invalid_message == NULL) invalid_message = strdup("Values must be an array of integer ids.");
18070 if (invalid_scope_message == NULL) invalid_scope_message = strdup("Values must belong to the current tenant context.");
18071 if (relation_field != NULL) {
18073 generated_surface_report_add_field_error_runtime(mut_doc, mut_root, relation_field, invalid_rule_id, "invalid_value", invalid_message);
18074 } else if (related_collection != NULL && related_collection[0] != '\0' && related_scope_field != NULL && related_scope_field[0] != '\0') {
18075 yyjson_val *related_rows = state_array_runtime(state_root, related_collection);
18076 yyjson_val *context_id_value = native_json_obj_get(context_root, "context_id");
18077 yyjson_arr_iter array_iter;
18078 yyjson_val *entry = NULL;
18079 int invalid_scope = 0;
18080 if (relation_value != NULL && yyjson_is_arr(relation_value) && context_id_value != NULL && !yyjson_is_null(context_id_value) && related_rows != NULL && yyjson_is_arr(related_rows) && yyjson_arr_iter_init(relation_value, &array_iter)) {
18081 while ((entry = yyjson_arr_iter_next(&array_iter)) != NULL && !invalid_scope) {
18082 long wanted = 0;
18083 yyjson_arr_iter rows_iter;
18084 yyjson_val *row = NULL;
18085 int found_in_scope = 0;
18086 if (!long_from_value_runtime(entry, &wanted) || !yyjson_arr_iter_init(related_rows, &rows_iter)) {
18087 continue;
18088 }
18089 while ((row = yyjson_arr_iter_next(&rows_iter)) != NULL) {
18090 long row_id = 0;
18091 if (!yyjson_is_obj(row) || !long_from_value_runtime(native_json_obj_get(row, "id"), &row_id) || row_id != wanted) {
18092 continue;
18093 }
18094 if (surface_context_match_runtime(row, related_scope_field, context_id_value)) {
18095 found_in_scope = 1;
18096 }
18097 break;
18098 }
18099 if (!found_in_scope) {
18100 invalid_scope = 1;
18101 }
18102 }
18103 }
18104 if (invalid_scope) {
18105 generated_surface_report_add_field_error_runtime(mut_doc, mut_root, relation_field, invalid_scope_rule_id, "invalid_scope", invalid_scope_message);
18106 }
18107 }
18108 }
18109 free(relation_field);
18110 free(related_collection);
18111 free(related_scope_field);
18112 free(invalid_rule_id);
18113 free(invalid_scope_rule_id);
18114 free(invalid_message);
18115 free(invalid_scope_message);
18116 }
18117 }
18118 rules = native_json_obj_get_array(surface_root, "uniqueness_rules");
18119 if (rules != NULL && yyjson_arr_iter_init(rules, &iter)) {
18120 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
18121 yyjson_val *fields = native_json_obj_get_array(item, "fields");
18122 yyjson_val *casefold_fields = native_json_obj_get_array(item, "casefold_fields");
18123 char *rule_id = native_json_obj_get_string_dup(item, "rule_id");
18124 char *message = native_json_obj_get_string_dup(item, "message");
18125 char *first_field = NULL;
18126 char *collection_name = native_json_obj_get_string_dup(surface_root, "entity_collection");
18127 yyjson_val *records = collection_name != NULL ? state_array_runtime(state_root, collection_name) : NULL;
18128 yyjson_arr_iter records_iter;
18129 yyjson_val *record = NULL;
18130 int duplicate_found = 0;
18131 if (rule_id == NULL) rule_id = strdup("unique_constraint");
18132 if (message == NULL) message = strdup("Unique constraint violated.");
18133 if (fields != NULL && yyjson_is_arr(fields) && yyjson_arr_size(fields) > 0) {
18134 yyjson_val *first_field_val = yyjson_arr_get_first(fields);
18135 first_field = yyjson_is_str(first_field_val) ? strdup(yyjson_get_str(first_field_val)) : strdup("value");
18136 } else {
18137 first_field = strdup("value");
18138 }
18139 if (fields != NULL && yyjson_is_arr(fields) && records != NULL && yyjson_is_arr(records) && yyjson_arr_iter_init(records, &records_iter)) {
18140 while ((record = yyjson_arr_iter_next(&records_iter)) != NULL && !duplicate_found) {
18141 yyjson_arr_iter field_iter;
18142 yyjson_val *field_item = NULL;
18143 int matches_all = 1;
18144 long record_id = 0;
18145 if (!yyjson_is_obj(record)) {
18146 continue;
18147 }
18148 if (has_current_id && long_from_value_runtime(native_json_obj_get(record, "id"), &record_id) && record_id == current_id) {
18149 continue;
18150 }
18151 if (yyjson_arr_iter_init(fields, &field_iter)) {
18152 while ((field_item = yyjson_arr_iter_next(&field_iter)) != NULL) {
18153 const char *field_name = yyjson_is_str(field_item) ? yyjson_get_str(field_item) : NULL;
18154 char *candidate_text = NULL;
18155 char *record_text = NULL;
18156 if (field_name == NULL) {
18157 continue;
18158 }
18159 candidate_text = json_value_tostring_dup_runtime(native_json_obj_get(candidate_root, field_name));
18160 record_text = json_value_tostring_dup_runtime(native_json_obj_get(record, field_name));
18161 if (generated_surface_array_contains_string_runtime(casefold_fields, field_name)) {
18162 if (candidate_text != NULL) {
18163 size_t i = 0; for (i = 0; candidate_text[i] != '\0'; i++) candidate_text[i] = (char)tolower((unsigned char)candidate_text[i]);
18164 }
18165 if (record_text != NULL) {
18166 size_t i = 0; for (i = 0; record_text[i] != '\0'; i++) record_text[i] = (char)tolower((unsigned char)record_text[i]);
18167 }
18168 }
18169 if ((candidate_text == NULL ? "" : candidate_text)[0] != '\0' || (record_text == NULL ? "" : record_text)[0] != '\0') {
18170 if (!streq(candidate_text != NULL ? candidate_text : "", record_text != NULL ? record_text : "")) {
18171 matches_all = 0;
18172 }
18173 } else if (!streq(candidate_text != NULL ? candidate_text : "", record_text != NULL ? record_text : "")) {
18174 matches_all = 0;
18175 }
18176 free(candidate_text);
18177 free(record_text);
18178 if (!matches_all) {
18179 break;
18180 }
18181 }
18182 }
18183 if (matches_all) {
18184 duplicate_found = 1;
18185 }
18186 }
18187 }
18188 if (duplicate_found) {
18189 generated_surface_report_add_field_error_runtime(mut_doc, mut_root, first_field != NULL ? first_field : "value", rule_id, "conflict", message);
18190 }
18191 free(rule_id);
18192 free(message);
18193 free(first_field);
18194 free(collection_name);
18195 }
18196 }
18197 yyjson_mut_doc_set_root(mut_doc, mut_root);
18198 code = emit_compact_json_mut_value_runtime(mut_root);
18199 generated_surface_validation_cleanup:
18200 native_json_doc_free(state_doc);
18201 native_json_doc_free(surface_doc);
18202 native_json_doc_free(context_doc);
18203 native_json_doc_free(candidate_doc);
18204 native_json_doc_free(report_doc);
18205 yyjson_mut_doc_free(mut_doc);
18206 return code;
18207}
18208
18209static int handle_generated_surface_mutate_command(int argc, char **argv) {
18210 const char *state_path = json_option_value_runtime(argc, argv, "--state");
18211 const char *surface_json = json_option_value_runtime(argc, argv, "--surface-json");
18212 const char *context_json = json_option_value_runtime(argc, argv, "--context-json");
18213 const char *operation_name = json_option_value_runtime(argc, argv, "--operation");
18214 const char *candidate_json = json_option_value_runtime(argc, argv, "--candidate-json");
18215 const char *existing_item_json = json_option_value_runtime(argc, argv, "--existing-item-json");
18216 const char *user_json = json_option_value_runtime(argc, argv, "--user-json");
18217 const char *created_at = json_option_value_runtime(argc, argv, "--created-at");
18218 yyjson_doc *state_doc = NULL;
18219 yyjson_doc *surface_doc = NULL;
18220 yyjson_doc *context_doc = NULL;
18221 yyjson_doc *candidate_doc = NULL;
18222 yyjson_doc *existing_doc = NULL;
18223 yyjson_doc *user_doc = NULL;
18224 yyjson_mut_doc *mut_doc = NULL;
18225 yyjson_val *state_root = NULL;
18226 yyjson_val *surface_root = NULL;
18227 yyjson_val *context_root = NULL;
18228 yyjson_val *candidate_root = NULL;
18229 yyjson_val *existing_root = NULL;
18230 yyjson_val *user_root = NULL;
18231 yyjson_mut_val *mut_root = NULL;
18232 yyjson_mut_val *section = NULL;
18233 yyjson_mut_val *item_mut = NULL;
18234 yyjson_mut_val *record_mut = NULL;
18235 char *collection_name = NULL;
18236 char *identity_field = NULL;
18237 char *delete_strategy = NULL;
18238 char *soft_delete_field = NULL;
18239 long record_id = 0;
18240 int normalize_after = 0;
18241 int code = 69;
18242 if (state_path == NULL || surface_json == NULL || context_json == NULL || operation_name == NULL || operation_name[0] == '\0') {
18243 return 64;
18244 }
18245 state_doc = native_json_doc_load_file(state_path);
18246 surface_root = load_root_from_text_runtime(surface_json, &surface_doc);
18247 context_root = load_root_from_text_runtime(context_json, &context_doc);
18248 if (user_json != NULL && user_json[0] != '\0') {
18249 user_root = load_root_from_text_runtime(user_json, &user_doc);
18250 }
18251 if (candidate_json != NULL && candidate_json[0] != '\0') {
18252 candidate_root = load_root_from_text_runtime(candidate_json, &candidate_doc);
18253 }
18254 if (existing_item_json != NULL && existing_item_json[0] != '\0') {
18255 existing_root = load_root_from_text_runtime(existing_item_json, &existing_doc);
18256 }
18257 state_root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
18258 if (state_root == NULL || surface_root == NULL || context_root == NULL || !yyjson_is_obj(state_root) || !yyjson_is_obj(surface_root) || !yyjson_is_obj(context_root)) {
18259 code = 64;
18260 goto generated_surface_mutate_cleanup;
18261 }
18262 collection_name = native_json_obj_get_string_dup(surface_root, "entity_collection");
18263 identity_field = native_json_obj_get_string_dup(surface_root, "identity_field");
18264 if (identity_field == NULL || identity_field[0] == '\0') {
18265 free(identity_field);
18266 identity_field = strdup("id");
18267 }
18268 if (collection_name == NULL || collection_name[0] == '\0') {
18269 code = 64;
18270 goto generated_surface_mutate_cleanup;
18271 }
18272 mut_doc = yyjson_doc_mut_copy(state_doc, NULL);
18273 mut_root = mut_doc != NULL ? yyjson_mut_doc_get_root(mut_doc) : NULL;
18274 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root)) {
18275 code = 69;
18276 goto generated_surface_mutate_cleanup;
18277 }
18278 section = ensure_mut_array_field_runtime(mut_doc, mut_root, collection_name);
18279 if (section == NULL || !yyjson_mut_is_arr(section)) {
18280 code = 69;
18281 goto generated_surface_mutate_cleanup;
18282 }
18283 if (streq(operation_name, "create")) {
18284 if (candidate_root == NULL || !yyjson_is_obj(candidate_root)) {
18285 code = 64;
18286 goto generated_surface_mutate_cleanup;
18287 }
18288 item_mut = yyjson_val_mut_copy(mut_doc, candidate_root);
18289 if (item_mut == NULL || !yyjson_mut_is_obj(item_mut)) {
18290 code = 69;
18291 goto generated_surface_mutate_cleanup;
18292 }
18293 record_id = max_id_in_state_section_runtime(state_root, collection_name) + 1;
18294 if (!yyjson_mut_obj_put(item_mut, yyjson_mut_strcpy(mut_doc, identity_field), yyjson_mut_int(mut_doc, record_id)) || !yyjson_mut_arr_append(section, item_mut)) {
18295 code = 69;
18296 goto generated_surface_mutate_cleanup;
18297 }
18298 normalize_after = 1;
18299 } else if (streq(operation_name, "update")) {
18300 if (candidate_root == NULL || !yyjson_is_obj(candidate_root)) {
18301 code = 64;
18302 goto generated_surface_mutate_cleanup;
18303 }
18304 record_id = native_json_obj_get_long_default(candidate_root, identity_field, 0, NULL);
18305 if (record_id <= 0) {
18306 code = 64;
18307 goto generated_surface_mutate_cleanup;
18308 }
18309 record_mut = find_mut_array_item_by_long_field_runtime(section, identity_field, record_id);
18310 if (record_mut == NULL || !merge_patch_into_mut_object_runtime(mut_doc, record_mut, candidate_root)) {
18311 code = record_mut == NULL ? 66 : 69;
18312 goto generated_surface_mutate_cleanup;
18313 }
18314 item_mut = record_mut;
18315 normalize_after = 1;
18316 } else if (streq(operation_name, "delete")) {
18317 if (existing_root == NULL || !yyjson_is_obj(existing_root)) {
18318 code = 64;
18319 goto generated_surface_mutate_cleanup;
18320 }
18321 record_id = native_json_obj_get_long_default(existing_root, identity_field, 0, NULL);
18322 if (record_id <= 0) {
18323 code = 64;
18324 goto generated_surface_mutate_cleanup;
18325 }
18326 delete_strategy = generated_surface_delete_strategy_runtime(surface_root);
18327 if (delete_strategy == NULL || streq(delete_strategy, "unsupported")) {
18328 code = 66;
18329 goto generated_surface_mutate_cleanup;
18330 }
18331 if (streq(delete_strategy, "soft")) {
18332 soft_delete_field = native_json_obj_get_string_dup(surface_root, "soft_delete_field");
18333 record_mut = find_mut_array_item_by_long_field_runtime(section, identity_field, record_id);
18334 if (soft_delete_field == NULL || soft_delete_field[0] == '\0' || record_mut == NULL || !yyjson_mut_obj_put(record_mut, yyjson_mut_strcpy(mut_doc, soft_delete_field), yyjson_mut_bool(mut_doc, 0))) {
18335 code = record_mut == NULL ? 66 : 69;
18336 goto generated_surface_mutate_cleanup;
18337 }
18338 item_mut = record_mut;
18339 } else {
18340 size_t idx = 0;
18341 size_t max = 0;
18342 yyjson_mut_val *entry = NULL;
18343 int removed = 0;
18344 yyjson_mut_arr_foreach(section, idx, max, entry) {
18345 if (yyjson_mut_is_obj(entry) && value_long_equals_runtime((yyjson_val *)yyjson_mut_obj_get(entry, identity_field), record_id)) {
18346 item_mut = yyjson_val_mut_copy(mut_doc, (yyjson_val *)entry);
18347 yyjson_mut_arr_remove(section, idx);
18348 removed = 1;
18349 break;
18350 }
18351 }
18352 if (!removed) {
18353 code = 66;
18354 goto generated_surface_mutate_cleanup;
18355 }
18356 normalize_after = 1;
18357 }
18358 } else {
18359 code = 64;
18360 goto generated_surface_mutate_cleanup;
18361 }
18362 if (!generated_surface_append_audit_event_mut_runtime(mut_doc, mut_root, surface_root, operation_name, user_root, context_root, (yyjson_val *)item_mut, created_at != NULL ? created_at : "")) {
18363 code = 69;
18364 goto generated_surface_mutate_cleanup;
18365 }
18366 if (normalize_after && !generated_surface_normalize_order_mut_runtime(mut_doc, mut_root, surface_root, context_root)) {
18367 code = 69;
18368 goto generated_surface_mutate_cleanup;
18369 }
18370 code = write_mutable_json_doc_runtime(mut_doc, state_path);
18371 if (code != 0) {
18372 goto generated_surface_mutate_cleanup;
18373 }
18374 code = emit_compact_json_mut_value_runtime(item_mut);
18375 generated_surface_mutate_cleanup:
18376 free(collection_name);
18377 free(identity_field);
18378 free(delete_strategy);
18379 free(soft_delete_field);
18380 native_json_doc_free(state_doc);
18381 native_json_doc_free(surface_doc);
18382 native_json_doc_free(context_doc);
18383 native_json_doc_free(candidate_doc);
18384 native_json_doc_free(existing_doc);
18385 native_json_doc_free(user_doc);
18386 yyjson_mut_doc_free(mut_doc);
18387 return code;
18388}
18389
18390int handle_native_json_command(int argc, char **argv) {
18391 static const NativeJsonCommandDispatchEntry command_entries[] = {
18392 { "bundle", "manifest-slice-order", 2, handle_bundle_manifest_slice_order_command },
18393 { "bundle", "manifest-layer-for-slice", 2, handle_bundle_manifest_layer_for_slice_command },
18394 { "request", "compact-json", 2, handle_request_compact_json_command },
18395 { "request", "compact-object", 2, handle_request_compact_object_command },
18396 { "request", "compact-object-strict", 2, handle_request_compact_object_strict_command },
18397 { "request", "field", 2, handle_request_field_command },
18398 { "request", "form-field", 2, handle_request_form_field_command },
18399 { "request", "cookie-value", 2, handle_request_cookie_value_command },
18400 { "request", "form-value", 2, handle_request_form_value_command },
18401 { "request", "query-value", 2, handle_request_query_value_command },
18402 { "request", "checkbox-id-array-json", 2, handle_request_checkbox_id_array_json_command },
18403 { "request", "booking-options-json-from-form", 2, handle_request_booking_options_json_from_form_command },
18404 { "request", "lines-to-booking-options-json", 2, handle_request_lines_to_booking_options_json_command },
18405 { "manifest", "fixture-path-raw", 2, handle_manifest_fixture_path_raw_command },
18406 { "manifest", "app-root", 2, handle_manifest_app_root_command },
18407 { "form", "field-json", 2, handle_form_field_json_command },
18408 { "form", "field-json-lines", 2, handle_form_field_json_lines_command },
18409 { "form", "options-csv", 2, handle_form_options_csv_command },
18410 { "form", "options-html", 2, handle_form_options_html_command },
18411 { "object", "field-string", 2, handle_object_field_string_command },
18412 { "object", "field-number", 2, handle_object_field_number_command },
18413 { "object", "field-text", 2, handle_object_field_text_command },
18414 { "object", "path-json", 2, handle_object_path_json_command },
18415 { "object", "path-string", 2, handle_object_path_string_command },
18416 { "object", "path-number", 2, handle_object_path_number_command },
18417 { "object", "path-bool", 2, handle_object_path_bool_command },
18418 { "object", "path-array-length", 2, handle_object_path_array_length_command },
18419 { "object", "array-item-json-lines", 2, handle_object_array_item_json_lines_command },
18420 { "object", "array-lines", 2, handle_object_array_lines_command },
18421 { "object", "relative-path-array-to-absolute-json", 2, handle_object_relative_path_array_to_absolute_json_command },
18422 { "object", "array-length", 2, handle_object_array_length_command },
18423 { "object", "array-all-integers", 2, handle_object_array_all_integers_command },
18424 { "object", "merge", 2, handle_object_merge_command },
18425 { "object", "build", 2, handle_object_build_command },
18426 { "template", "render-path", 2, handle_template_render_path_command },
18427 { "template", "render-file", 2, handle_template_render_file_command },
18428 { "template", "declared-page-contract", 2, handle_template_declared_page_contract_command },
18429 { "template", "declared-fragment-path", 2, handle_template_declared_fragment_path_command },
18430 { "template", "apply-base-path", 2, handle_template_apply_base_path_command },
18431 { "template", "layout-page-html", 2, handle_template_layout_page_html_command },
18432 { "template", "layout-page-json", 2, handle_template_layout_page_json_command },
18433 { "template", "login-page-json", 2, handle_template_login_page_json_command },
18434 { "template", "register-page-json", 2, handle_template_register_page_json_command },
18435 { "template", "password-reset-request-page-json", 2, handle_template_password_reset_request_page_json_command },
18436 { "template", "password-reset-sent-page-json", 2, handle_template_password_reset_sent_page_json_command },
18437 { "template", "password-reset-complete-page-json", 2, handle_template_password_reset_complete_page_json_command },
18438 { "template", "invite-created-page-json", 2, handle_template_invite_created_page_json_command },
18439 { "template", "invite-accept-page-json", 2, handle_template_invite_accept_page_json_command },
18440 { "validation", "empty-report", 2, handle_validation_empty_report_command },
18441 { "validation", "add-field-error", 2, handle_validation_add_field_error_command },
18442 { "validation", "add-form-error", 2, handle_validation_add_form_error_command },
18443 { "validation", "has-errors", 2, handle_validation_has_errors_command },
18444 { "validation", "summary-text", 2, handle_validation_summary_text_command },
18445 { "validation", "field-list-html", 2, handle_validation_field_list_html_command },
18446 { "validation", "error-envelope", 2, handle_validation_error_envelope_command },
18447 { "error", "envelope", 2, handle_error_envelope_command },
18448 { "dynamic", "config-value", 2, handle_dynamic_config_value_command },
18449 { "dynamic", "file-signature", 2, handle_dynamic_file_signature_command },
18450 { "dynamic", "sqlite-runtime-ready", 2, handle_dynamic_sqlite_runtime_ready_command },
18451 { "dynamic", "sqlite-runtime-prepare", 2, handle_dynamic_sqlite_runtime_prepare_command },
18452 { "dynamic", "postgresql-runtime-ready", 2, handle_dynamic_postgresql_runtime_ready_command },
18453 { "dynamic", "postgresql-runtime-prepare", 2, handle_dynamic_postgresql_runtime_prepare_command },
18454 { "dynamic", "relational-backend", 2, handle_dynamic_relational_backend_command },
18455 { "dynamic", "relational-sqlite-path", 2, handle_dynamic_relational_sqlite_path_command },
18456 { "dynamic", "relational-runtime-export-json", 2, handle_dynamic_relational_runtime_export_json_command },
18457 { "dynamic", "relational-runtime-audit-count", 2, handle_dynamic_relational_runtime_audit_count_command },
18458 { "dynamic", "relational-runtime-import-state", 2, handle_dynamic_relational_runtime_import_state_command },
18459 { "dynamic", "safe-upload-filename", 2, handle_dynamic_safe_upload_filename_command },
18460 { "dynamic", "media-disk-path", 2, handle_dynamic_media_disk_path_command },
18461 { "dynamic", "ensure-business-media-tree", 2, handle_dynamic_ensure_business_media_tree_command },
18462 { "dynamic", "remove-media-url-file", 2, handle_dynamic_remove_media_url_file_command },
18463 { "dynamic", "store-uploaded-media", 2, handle_dynamic_store_uploaded_media_command },
18464 { "dynamic", "multipart-filename", 2, handle_dynamic_multipart_filename_command },
18465 { "dynamic", "multipart-extract-text", 2, handle_dynamic_multipart_extract_text_command },
18466 { "dynamic", "multipart-extract-file", 2, handle_dynamic_multipart_extract_file_command },
18467 { "db", "sqlite-table-exists", 2, handle_db_sqlite_table_exists_command },
18468 { "db", "sqlite-table-row-count", 2, handle_db_sqlite_table_row_count_command },
18469 { "db", "sqlite-table-count", 2, handle_db_sqlite_table_count_command },
18470 { "db", "sqlite-migration-applied-count", 2, handle_db_sqlite_migration_applied_count_command },
18471 { "db", "sqlite-migration-applied-schema-version", 2, handle_db_sqlite_migration_applied_schema_version_command },
18472 { "db", "sqlite-migration-applied-checksum", 2, handle_db_sqlite_migration_applied_checksum_command },
18473 { "db", "sqlite-reconcile-migration-checksum", 2, handle_db_sqlite_reconcile_migration_checksum_command },
18474 { "db", "sqlite-record-migration", 2, handle_db_sqlite_record_migration_command },
18475 { "db", "sqlite-audit-event-count", 2, handle_db_sqlite_audit_event_count_command },
18476 { "db", "sqlite-query-value", 2, handle_db_sqlite_query_value_command },
18477 { "db", "sqlite-exec", 2, handle_db_sqlite_exec_command },
18478 { "db", "sqlite-exec-file", 2, handle_db_sqlite_exec_file_command },
18479 { "db", "postgresql-database-exists", 2, handle_db_postgresql_database_exists_command },
18480 { "db", "postgresql-create-database-if-missing", 2, handle_db_postgresql_create_database_if_missing_command },
18481 { "db", "postgresql-table-exists", 2, handle_db_postgresql_table_exists_command },
18482 { "db", "postgresql-table-row-count", 2, handle_db_postgresql_table_row_count_command },
18483 { "db", "postgresql-public-table-count", 2, handle_db_postgresql_public_table_count_command },
18484 { "db", "postgresql-migration-applied-count", 2, handle_db_postgresql_migration_applied_count_command },
18485 { "db", "postgresql-migration-applied-schema-version", 2, handle_db_postgresql_migration_applied_schema_version_command },
18486 { "db", "postgresql-migration-applied-checksum", 2, handle_db_postgresql_migration_applied_checksum_command },
18487 { "db", "postgresql-reconcile-migration-checksum", 2, handle_db_postgresql_reconcile_migration_checksum_command },
18488 { "db", "postgresql-audit-event-count", 2, handle_db_postgresql_audit_event_count_command },
18489 { "db", "postgresql-query-value", 2, handle_db_postgresql_query_value_command },
18490 { "db", "postgresql-exec", 2, handle_db_postgresql_exec_command },
18491 { "db", "postgresql-exec-file", 2, handle_db_postgresql_exec_file_command },
18492 { "file", "migration-backend-list-lines", 2, handle_file_migration_backend_list_lines_command },
18493 { "file", "migration-rows-lines", 2, handle_file_migration_rows_lines_command },
18494 { "file", "migration-backend-record-json", 2, handle_file_migration_backend_record_json_command },
18495 { "file", "migration-backend-files-json", 2, handle_file_migration_backend_files_json_command },
18496 { "file", "normalize-ucal-state", 2, handle_file_normalize_ucal_state_command },
18497 { "file", "generated-surface-match", 2, handle_file_generated_surface_match_command },
18498 { "file", "compact-object", 2, handle_file_compact_object_command },
18499 { "file", "field-string", 2, handle_file_field_string_command },
18500 { "file", "field-number", 2, handle_file_field_number_command },
18501 { "file", "section-length", 2, handle_file_section_length_command },
18502 { "generated-surface", "list-columns-json", 2, handle_generated_surface_list_columns_json_command },
18503 { "generated-surface", "params-json", 2, handle_generated_surface_params_json_command },
18504 { "generated-surface", "display-value", 2, handle_generated_surface_display_value_command },
18505 { "generated-surface", "select-options-html", 2, handle_generated_surface_select_options_html_command },
18506 { "generated-surface", "relation-checkboxes-html", 2, handle_generated_surface_relation_checkboxes_html_command },
18507 { "generated-surface", "manage-table-json", 2, handle_generated_surface_manage_table_json_command },
18508 { "generated-surface", "form-fields-html", 2, handle_generated_surface_form_fields_html_command },
18509 { "generated-surface", "filter-controls-html", 2, handle_generated_surface_filter_controls_html_command },
18510 { "generated-surface", "sort-options-html", 2, handle_generated_surface_sort_options_html_command },
18511 { "generated-surface", "manage-page-json", 2, handle_generated_surface_manage_page_json_command },
18512 { "generated-surface", "edit-page-json", 2, handle_generated_surface_edit_page_json_command },
18513 { "generated-surface", "context", 2, handle_generated_surface_context_command },
18514 { "generated-surface", "list-result", 2, handle_generated_surface_list_result_command },
18515 { "generated-surface", "coerce-candidate", 2, handle_generated_surface_coerce_candidate_command },
18516 { "generated-surface", "submitted", 2, handle_generated_surface_submitted_command },
18517 { "generated-surface", "default-candidate", 2, handle_generated_surface_default_candidate_command },
18518 { "generated-surface", "validation-report", 2, handle_generated_surface_validation_report_command },
18519 { "generated-surface", "mutate", 2, handle_generated_surface_mutate_command },
18520 { "state", NULL, 1, handle_state_named_command }
18521 };
18522 if (argc < 1 || argv == NULL) {
18523 return 78;
18524 }
18526 argc,
18527 argv,
18528 command_entries,
18529 sizeof(command_entries) / sizeof(command_entries[0])
18530 );
18531}
char * app_local_conf_path_native(const char *app_root)
Builds the path to an app-local runtime configuration file.
char * resolve_conf_relative_path_native(const char *conf_path, const char *raw_value)
Resolves a config value relative to the directory that owns a config file.
Declares app-local runtime configuration helpers and the app-local configuration record.
char * native_dynamic_config_merged_value_dup(const char *app_root, const char *runtime_conf_path, const char *key)
Loads one merged dynamic config value with runtime-conf precedence over app-local config.
Declares Pharos-owned dynamic runtime config resolution helpers.
int remove_media_url_file(const char *artifact_dir, const char *media_url)
Removes a managed business media file from an app artifact when it exists.
char * store_uploaded_media(const char *artifact_dir, const char *content_type, const char *body_path, const char *field_name, const char *target_url_dir, const char *fallback_filename)
Extracts a multipart upload into the app artifact media tree and returns the resulting media URL.
char * safe_upload_filename_dup(const char *raw_name)
Sanitises a raw upload filename into a safe filesystem name.
int ensure_business_media_tree(const char *artifact_dir, const char *business_slug)
Ensures the managed business media subtree exists under an app artifact.
char * media_disk_path_dup(const char *artifact_dir, const char *media_url)
Resolves a media URL into an on-disk path inside an app artifact directory.
Media and static asset serving for in-process dynamic handling.
int native_dynamic_multipart_extract_file(const unsigned char *body, size_t body_len, const char *content_type, const char *field_name, const char *output_path)
Extracts file content from a multipart file part and writes it to disk.
char * native_dynamic_multipart_extract_text(const unsigned char *body, size_t body_len, const char *content_type, const char *field_name)
Extracts a text value from a multipart form field.
char * native_dynamic_multipart_filename(const unsigned char *body, size_t body_len, const char *content_type, const char *field_name)
Extracts the uploaded filename for a named multipart file part.
Pharos-owned multipart form-data parsing for in-process dynamic handling.
#define PHAROS_PGRES_TUPLES_OK_RUNTIME
int native_dynamic_relational_runtime_import_state(const char *app_root, const char *runtime_conf_path, const char *app_id, const char *state_path, const char *expected_audit_count)
Imports canonical runtime state JSON into the configured relational backend.
int native_dynamic_relational_postgresql_runtime_ready(const char *app_root, const char *runtime_conf_path)
Returns whether PostgreSQL runtime prerequisites are satisfied.
int native_dynamic_relational_postgresql_runtime_prepare(const char *app_root, const char *runtime_conf_path)
Prepares the PostgreSQL schema, seed data, and migration ledger for one dynamic app.
int native_dynamic_relational_sqlite_runtime_prepare(const char *app_root, const char *runtime_conf_path, const char *app_id)
Prepares the sqlite schema, seed data, and migration ledger for one dynamic app.
#define PHAROS_PGRES_COMMAND_OK_RUNTIME
char * native_dynamic_relational_runtime_audit_count_dup(const char *app_root, const char *runtime_conf_path, const char *app_id)
Returns the current audit-event count from the configured relational backend.
int native_dynamic_relational_sqlite_runtime_ready(const char *app_root, const char *runtime_conf_path, const char *app_id)
Returns whether sqlite runtime prerequisites are satisfied.
char * native_dynamic_relational_runtime_export_json_dup(const char *app_root, const char *runtime_conf_path, const char *app_id)
Exports canonical runtime state JSON from the configured relational backend.
struct pg_conn PGconn
#define PHAROS_CONN_STATUS_OK_RUNTIME
char * native_dynamic_relational_backend_dup(const char *app_root, const char *runtime_conf_path)
Returns the configured relational backend when it is recognized.
struct pg_result PGresult
char * native_dynamic_relational_sqlite_path_dup(const char *app_root, const char *runtime_conf_path, const char *app_id)
Returns the effective sqlite database path for a dynamic app.
#define PHAROS_PGRES_SINGLE_TUPLE_RUNTIME
Declares Pharos-owned native relational runtime helpers for dynamic apps.
char * native_dynamic_form_value(const char *body, const char *key)
Extracts a value from a URL-encoded form body.
char * native_dynamic_query_value(const char *query, const char *key)
Extracts a parameter value from a query string.
char * native_dynamic_cookie_value(const char *cookie_header, const char *name)
Extracts a cookie value from a Cookie header string.
Request field extraction helpers for in-process dynamic handling.
Declares template rendering functions for the in-process dynamic runtime.
int ensure_directory(const char *path)
Creates a directory path and any missing parent directories.
Definition native_fs.c:31
int write_text_file(const char *path, const char *content)
Writes a text buffer to a file path.
Definition native_fs.c:66
Declares native filesystem helper routines.
char * conf_lookup_value(const char *path, const char *key)
Reads a Pharos conf file and looks up one key.
Declares lightweight JSON and conf value lookup helpers.
static int parse_ymd_runtime(const char *date_value, int *year_out, int *month_out, int *day_out)
static int json_value_list_append_runtime(json_value_list_runtime *list, yyjson_val *item)
static int handle_template_apply_base_path_command(int argc, char **argv)
static int digits_only_text_runtime(const char *text)
static char * resolve_dynamic_config_value_runtime(const char *conf_path, const char *key, const char *raw_value)
Resolves one raw dynamic config value against its owning config file when needed.
static int handle_dynamic_multipart_extract_text_command(int argc, char **argv)
Extracts a text value from a multipart form field.
static char * render_business_dashboard_cards_html_dup_runtime(yyjson_val *root, yyjson_val *tenant_ids, const char *app_root)
static long payload_first_long_runtime(yyjson_val *payload, const char **fields, size_t field_count)
static int weekday_monday_index_runtime(int year, int month, int day)
static char * generated_surface_label_from_field_runtime(const char *field_name)
static int handle_state_event_stream_row_json_lines_command(yyjson_val *root, yyjson_val *subscription_scope, yyjson_val *filters, long since_id, long limit_value)
static int add_json_string_or_null_runtime(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, const char *value)
static char * template_value_text_dup_runtime(yyjson_val *value)
static int handle_db_postgresql_migration_applied_count_command(int argc, char **argv)
Emits one PostgreSQL migration applied count.
static int emit_business_admin_service_rows_json_runtime(yyjson_val *root, long business_id)
static int handle_validation_field_list_html_command(int argc, char **argv)
static int handle_object_path_string_command(int argc, char **argv)
static void month_shift_runtime(int year, int month, int shift, int *out_year, int *out_month)
static int append_escaped_sql_char_runtime(TextBufferRuntime *buffer, char value)
static char * dynamic_sqlite_path_dup_runtime(const char *app_root, const char *runtime_conf_path, const char *app_id)
static char * runtime_state_placeholder_sql_dup_runtime(const char *sql_template, const char *escaped_state_json)
Replaces the canonical runtime-state placeholder inside one declarative SQL template.
static int handle_object_path_number_command(int argc, char **argv)
static int handle_file_compact_object_command(int argc, char **argv)
char * business_calendar_render_page_params_json_dup_runtime(const char *state_path, const char *business_slug, const char *app_root, const char *selected_year_text, const char *selected_month_text, const char *selected_date_text)
static int template_reference_contains_unsafe_char_runtime(const char *text)
Checks whether a reference candidate contains unsafe control bytes.
static int handle_object_field_number_command(int argc, char **argv)
static const char * json_option_value_runtime(int argc, char **argv, const char *name)
char * business_dashboard_render_page_params_json_dup_runtime(const char *state_path, long user_id, const char *app_root)
Builds rendered business dashboard page params JSON from runtime state.
static int load_cli_postgresql_runtime_config(int argc, char **argv, DynamicPostgresqlRuntimeConfig *config)
Loads PostgreSQL connection settings from explicit JSON command options.
static int event_stream_allowed_by_scope_runtime(yyjson_val *subscription_scope, long event_user_id, long event_tenant_id)
static char * request_form_field_value_dup_runtime(const char *content_type, const char *form_body, const char *body_path, const char *field_name)
Resolves one request form field from urlencoded or multipart request inputs.
static char * json_value_tostring_dup_runtime(yyjson_val *value)
static int handle_request_query_value_command(int argc, char **argv)
Extracts a parameter value from a query string.
static char * dynamic_relational_runtime_audit_count_dup_runtime(const char *app_root, const char *runtime_conf_path, const char *app_id)
Returns the current audit-event count for one dynamic relational runtime using the configured backend...
static int trim_ascii_whitespace_range_runtime(const char *start, size_t len, const char **trimmed_start, size_t *trimmed_len)
unsigned char * read_file_bytes_dup_runtime(const char *path, size_t *out_size)
Reads one file into an owned binary buffer and appends a trailing NUL byte for text helpers.
static int handle_object_build_command(int argc, char **argv)
static int emit_explore_page_json_runtime(yyjson_val *root, const char *app_root)
Emits rendered explore-page template params using declarative business card fragments.
static int generated_surface_array_contains_string_runtime(yyjson_val *array, const char *wanted)
static int command_exists_in_path_runtime(const char *command)
static int handle_file_generated_surface_match_command(int argc, char **argv)
static int dispatch_native_json_command_entries(int argc, char **argv, const NativeJsonCommandDispatchEntry *entries, size_t entry_count)
char * profile_render_page_params_json_dup_runtime(const char *state_path, long user_id, const char *app_root, const char *payment_preference_options_html)
Builds rendered profile page params JSON from runtime state.
char * render_file_template_dup_runtime(const char *template_file, yyjson_val *params_root)
Reads a template file and renders it with JSON parameter substitution.
static int json_value_equals_text_runtime(yyjson_val *value, const char *expected)
static int handle_generated_surface_select_options_html_command(int argc, char **argv)
static int handle_db_sqlite_migration_applied_count_command(int argc, char **argv)
Emits one sqlite migration applied count.
static char * url_decode_dup_runtime(const char *text)
char * error_alert_html_dup_runtime(const char *message)
Renders an error alert.
static int duplicate_postgresql_runtime_config_with_database_runtime(const DynamicPostgresqlRuntimeConfig *source, const char *database_name, DynamicPostgresqlRuntimeConfig *copy_out)
Duplicates one PostgreSQL runtime configuration while replacing the database name.
static char * sqlite_query_value_runtime(const char *sqlite_path, const char *sql_text)
static int handle_template_login_page_json_command(int argc, char **argv)
static yyjson_val * state_find_user_by_id_runtime(yyjson_val *root, long user_id)
static TemplatePlaceholderKindRuntime template_placeholder_kind_runtime(const char *placeholder_name, const char **param_name)
Parses one placeholder token into its rendering mode and JSON key name.
static int handle_db_sqlite_migration_applied_schema_version_command(int argc, char **argv)
Emits one sqlite migration applied schema version.
static int handle_dynamic_sqlite_runtime_ready_command(int argc, char **argv)
Emits whether the sqlite dynamic runtime prerequisites are currently satisfied.
static int sqlite_apply_file_list_runtime(const char *sqlite_path, const char *app_root, yyjson_val *list)
static int text_buffer_reserve_runtime(TextBufferRuntime *buffer, size_t needed_length)
static int emit_json_string_runtime(const char *text)
static yyjson_val * find_active_token_record_runtime(yyjson_val *array_value, const char *token_hash, const char *now_iso)
static int streq_casefold_runtime(const char *left, const char *right)
char * join_base_relative_path_dup_runtime(const char *base_path, const char *relative_path)
Joins a base path and a relative path into one string.
static char * render_template_text_with_default_kind_dup_runtime(const char *template_text, yyjson_val *params_root, TemplatePlaceholderKindRuntime default_kind)
Renders one template string using one caller-selected default placeholder mode.
static void free_event_stream_row_list_runtime(event_stream_row_list_runtime *list)
static int handle_request_lines_to_booking_options_json_command(int argc, char **argv)
static int handle_db_sqlite_exec_command(int argc, char **argv)
Executes one sqlite SQL text payload.
static int generated_surface_text_is_email_runtime(const char *text)
static int handle_file_migration_backend_files_json_command(int argc, char **argv)
static const char * day_abbrev_for_ymd_runtime(int year, int month, int day)
static int handle_file_migration_rows_lines_command(int argc, char **argv)
static char * build_generated_surface_sort_options_html_runtime(yyjson_val *surface_root, const char *raw_query)
static long max_id_in_state_section_runtime(yyjson_val *root, const char *section_name)
static int handle_object_relative_path_array_to_absolute_json_command(int argc, char **argv)
static int handle_form_options_csv_command(int argc, char **argv)
static char * resource_kind_from_resource_id_runtime(const char *resource_id)
static void surface_sort_items_runtime(json_value_list_runtime *list, yyjson_val *sort_config)
static int write_temp_text_file_runtime(const char *text, char **path_out)
static int handle_dynamic_multipart_filename_command(int argc, char **argv)
Extracts the uploaded filename from a multipart file part.
static int emit_technician_edit_page_json_runtime(yyjson_val *root, const char *slug, long technician_id)
char * business_detail_admin_render_page_params_json_dup_runtime(const char *state_path, const char *business_slug, const char *app_root)
Builds rendered business admin page params JSON from runtime state.
static int handle_dynamic_sqlite_runtime_prepare_command(int argc, char **argv)
static int handle_dynamic_remove_media_url_file_command(int argc, char **argv)
Removes one managed business media file from an app artifact when it exists.
static int handle_generated_surface_list_result_command(int argc, char **argv)
static int handle_dynamic_relational_sqlite_path_command(int argc, char **argv)
Emits the effective sqlite path for a dynamic app.
static int json_array_contains_long_runtime(yyjson_val *array_value, long wanted)
static int handle_dynamic_postgresql_runtime_prepare_command(int argc, char **argv)
Prepares the PostgreSQL dynamic runtime schema, seed data, and migration ledger.
static char * ucal_runtime_state_export_sqlite_path_dup_runtime(const char *app_root)
Resolves the declarative sqlite export query path for UCAL runtime-state bridge export.
static int technician_booked_for_slot_runtime(yyjson_val *root, long technician_id, const char *date_value, const char *time_value, long duration_minutes)
char * business_landing_render_page_params_json_dup_runtime(const char *state_path, const char *business_slug, long user_id, const char *app_root)
static int days_in_month_runtime(int year, int month)
static int template_reference_has_scheme_runtime(const char *text)
Tests whether a reference begins with a URI scheme prefix.
static long state_tenant_id_for_business_id_runtime(yyjson_val *root, long business_id)
static int handle_db_sqlite_table_row_count_command(int argc, char **argv)
Emits one sqlite table row count.
static yyjson_val * object_value_at_path_runtime(yyjson_val *root, const char *path)
static char * render_business_admin_empty_state_dup_runtime(const char *app_root, const char *message)
static int handle_template_declared_page_contract_command(int argc, char **argv)
Emits one UCAL declared page contract as JSON.
static int seen_long_pair_runtime(long *values, size_t count, long first, long second)
char * appointments_render_page_params_json_dup_runtime(const char *state_path, long user_id, const char *app_root)
Builds rendered appointments page params JSON from runtime state.
static int handle_generated_surface_list_columns_json_command(int argc, char **argv)
static char * app_root_relative_path_dup_runtime(const char *app_root, const char *relative_path)
Joins an app root with one relative template path.
static char * dynamic_merged_config_value_runtime(const char *app_root, const char *runtime_conf_path, const char *key)
Loads one merged dynamic config value with runtime-conf precedence over app-local config.
static yyjson_mut_val * surface_build_active_filters_runtime(yyjson_mut_doc *doc, yyjson_val *surface_root, const char *raw_query)
static char * generated_surface_delete_button_label_dup_runtime(yyjson_val *surface_root)
static int emit_long_runtime(long value)
static int parse_hhmm_minutes_runtime(const char *value, int *minutes_out)
static int handle_generated_surface_relation_checkboxes_html_command(int argc, char **argv)
static int emit_business_technician_ids_lines_runtime(yyjson_val *root, long business_id)
static int emit_digit_bool_runtime(int truthy)
Emits one digit boolean result for shell callers.
static char * text_buffer_take_runtime(TextBufferRuntime *buffer)
static int handle_db_postgresql_public_table_count_command(int argc, char **argv)
Emits the current PostgreSQL public table count.
static int handle_file_field_number_command(int argc, char **argv)
static int emit_calendar_page_json_runtime(yyjson_val *root, long user_id, yyjson_val *tenant_ids, const char *app_root)
Emits rendered calendar-page template params using declarative booking and appointment fragments.
static int handle_db_sqlite_record_migration_command(int argc, char **argv)
Inserts one sqlite migration ledger record.
static int handle_db_postgresql_query_value_command(int argc, char **argv)
Emits the first-line output of one PostgreSQL query.
static int emit_compact_json_value_runtime(yyjson_val *value)
static char * migration_expected_checksum_dup_runtime(const char *app_root, yyjson_val *migration_record, const char *backend)
static int emit_sorted_section_lines_runtime(yyjson_val *root, const char *section_name)
static int emit_text_line_runtime(const char *text)
static int compare_item_id_runtime(const void *lhs_ptr, const void *rhs_ptr)
static yyjson_val * state_find_first_service_for_business_runtime(yyjson_val *root, long business_id)
static int surface_context_match_runtime(yyjson_val *item, const char *scope_field, yyjson_val *context_id_value)
static int handle_db_postgresql_exec_command(int argc, char **argv)
Executes one PostgreSQL SQL text payload.
static int emit_compact_json_mut_value_runtime(yyjson_mut_val *value)
static yyjson_val * surface_find_record_by_string_field_runtime(yyjson_val *root, const char *collection_name, const char *field_name, const char *expected_value)
char * render_path_template_text_dup_runtime(const char *template_text, yyjson_val *params_root)
Renders one path-oriented template text string with JSON substitution.
char * render_template_text_dup_runtime(const char *template_text, yyjson_val *params_root)
Renders one HTML template text string with JSON parameter substitution.
static yyjson_mut_val * find_mut_array_item_by_long_field_runtime(yyjson_mut_val *array_value, const char *field_name, long expected_value)
static int multipart_find_part_runtime(const unsigned char *body, size_t body_len, const char *content_type, const char *field_name, char **out_text, char **out_filename, const unsigned char **out_content_start, size_t *out_content_len)
Finds one multipart part by field name and optionally returns its filename and content range.
static int handle_dynamic_relational_runtime_audit_count_command(int argc, char **argv)
Emits the current audit-event count for a sqlite-backed dynamic relational runtime.
static long state_tenant_id_for_business_slug_runtime(yyjson_val *root, const char *slug)
static yyjson_val * state_find_technician_for_business_runtime(yyjson_val *root, long technician_id, long business_id)
static int digits_only_runtime(const char *text, size_t len)
static int handle_dynamic_config_value_command(int argc, char **argv)
Emits one merged dynamic config value.
static char * capture_checksum_output_runtime(const char *command, const char *arg1, const char *arg2, const char *file_path, int *code_out)
static int emit_template_page_params_json_runtime(const char *error_html, const char *next_input, const char *account_type_options_html, const char *preview_html, const char *reset_post_path, const char *invite_email, const char *invite_path, const char *return_path, const char *invite_kind, const char *target_html, const char *form_action)
static int template_url_reference_allowed_runtime(const char *text)
Tests whether one candidate URL reference is safe for HTML URL attributes.
static yyjson_val * find_array_item_by_long_field_runtime(yyjson_val *array_value, const char *field_name, long expected_value)
char * explore_render_page_params_json_dup_runtime(const char *state_path, const char *app_root)
Builds rendered appointments page params JSON from runtime state.
static char * render_technician_checkboxes_html_dup_runtime(yyjson_val *root, long business_id, long service_id, const char *empty_message, int select_all, const char *app_root)
static int handle_object_merge_command(int argc, char **argv)
static char * day_of_week_options_html_dup_runtime(const char *selected_day)
static int handle_generated_surface_sort_options_html_command(int argc, char **argv)
static int value_long_equals_runtime(yyjson_val *value, long expected_value)
static int emit_business_public_render_page_json_runtime(yyjson_val *root, const char *slug, long user_id, const char *app_root)
static int handle_db_sqlite_table_count_command(int argc, char **argv)
Emits one sqlite non-system table count.
static int merge_patch_into_mut_object_runtime(yyjson_mut_doc *mut_doc, yyjson_mut_val *target_obj, yyjson_val *patch_root)
static int handle_validation_add_field_error_command(int argc, char **argv)
char * html_escaped_dup_runtime(const char *text)
Escapes one plain-text string for safe HTML insertion.
static int surface_filter_item_runtime(yyjson_val *item, yyjson_val *filters)
static int handle_dynamic_relational_backend_command(int argc, char **argv)
Emits the configured dynamic relational backend when it is recognized.
static yyjson_val * migration_backend_record_runtime(yyjson_val *root, const char *backend_value)
static int dynamic_relational_runtime_import_state_runtime(const char *app_root, const char *runtime_conf_path, const char *app_id, const char *state_path, const char *expected_audit_count)
Imports canonical UCAL relational runtime state using the configured backend contract.
static char * ucal_runtime_state_export_postgresql_path_dup_runtime(const char *app_root)
Resolves the declarative postgresql export query path for UCAL runtime-state bridge export.
char * declared_page_relative_path_dup_runtime(const char *app_root, const char *page_id)
Resolves one UCAL declared page id to its app-relative page-spec path.
static int event_stream_matches_filters_runtime(yyjson_val *filters, const event_stream_row_runtime *row)
static int collect_business_service_list_runtime(yyjson_val *services, long business_id, business_admin_service_list_runtime *list)
static int run_sqlite_command_runtime(const char *sqlite_path, const char *sql_text, char **output_out)
Executes one sqlite SQL batch through the native sqlite3 C API.
static int handle_generated_surface_mutate_command(int argc, char **argv)
static int emit_business_public_page_json_runtime(yyjson_val *root, const char *slug, long user_id)
static int business_open_for_slot_runtime(yyjson_val *root, long business_id, const char *date_value, const char *time_value, long duration_minutes)
static int handle_template_password_reset_request_page_json_command(int argc, char **argv)
static int handle_generated_surface_manage_table_json_command(int argc, char **argv)
char * language_options_html_dup_runtime(const char *current_lang)
static char * service_media_url_dup_runtime(const char *business_slug, const char *media_url)
static int handle_db_sqlite_exec_file_command(int argc, char **argv)
Executes one sqlite SQL file.
static char * render_service_sort_order_options_html_dup_runtime(yyjson_val *root, long business_id, long selected_order, int include_append, const char *app_root)
static void free_business_admin_service_list_runtime(business_admin_service_list_runtime *list)
static int handle_template_register_page_json_command(int argc, char **argv)
char * business_edit_page_params_json_dup_runtime(const char *state_path, const char *business_slug)
Builds rendered business-edit page params JSON from runtime state.
static char * render_service_visual_html_dup_runtime(const char *app_root, const char *image_url, const char *service_name)
static int handle_state_named_command(int argc, char **argv)
static yyjson_val * state_find_business_by_slug_runtime(yyjson_val *root, const char *slug)
static char * sql_literal_escape_dup_runtime(const char *value)
static int handle_generated_surface_filter_controls_html_command(int argc, char **argv)
char * service_book_form_page_params_json_dup_runtime(const char *state_path, const char *business_slug, long service_id, const char *app_root, const char *error_message)
Builds rendered service-book-form page params JSON from runtime state.
static int handle_state_ucal_import_row_json_lines_command(yyjson_val *root, const char *section_name)
static char * cksum_signature_runtime(const char *value)
static char * resolve_command_path_runtime(const char *command)
Tests whether an executable command name resolves through PATH.
static int handle_dynamic_safe_upload_filename_command(int argc, char **argv)
Emits the safe Pharos filename for an uploaded media candidate.
static int handle_template_invite_created_page_json_command(int argc, char **argv)
static int handle_template_layout_page_json_command(int argc, char **argv)
static char * render_business_fragment_dup_runtime(const char *app_root, const char *fragment_name, yyjson_val *params_root)
static int handle_object_path_bool_command(int argc, char **argv)
static int handle_template_layout_page_html_command(int argc, char **argv)
static int generated_surface_relation_array_all_integers_runtime(yyjson_val *array)
static int handle_generated_surface_params_json_command(int argc, char **argv)
static const char * ucal_declared_page_relative_path_runtime(const char *page_id)
Resolves one UCAL declared page id to its app-relative page-spec path.
static int emit_business_dashboard_page_json_runtime(yyjson_val *root, yyjson_val *tenant_ids, const char *app_root)
static int text_buffer_append_html_escaped_runtime(TextBufferRuntime *buffer, const char *text)
static int handle_validation_error_envelope_command(int argc, char **argv)
static int emit_business_calendar_page_json_runtime(yyjson_val *root, const char *slug)
static char * generated_surface_display_value_dup_runtime(yyjson_val *item_root, const char *field_name)
static yyjson_val * state_find_google_calendar_connection_for_business_runtime(yyjson_val *root, long business_id)
static int handle_db_postgresql_exec_file_command(int argc, char **argv)
Executes one PostgreSQL SQL file.
static int compare_business_admin_service_runtime(const void *left_ptr, const void *right_ptr)
static yyjson_val * find_array_item_by_string_field_runtime(yyjson_val *array_value, const char *field_name, const char *expected_value)
static char * multipart_boundary_dup_runtime(const char *content_type)
Extracts the multipart boundary token from a content type value.
static int handle_generated_surface_edit_page_json_command(int argc, char **argv)
static int emit_service_edit_page_json_runtime(yyjson_val *root, const char *slug, long service_id, const char *app_root)
static int emit_technician_appointments_lines_runtime(yyjson_val *root, long technician_id, const char *date_value)
static int handle_db_postgresql_table_exists_command(int argc, char **argv)
Emits whether one PostgreSQL table exists in the public schema.
static int handle_dynamic_ensure_business_media_tree_command(int argc, char **argv)
Ensures the managed business media subtree exists under an app artifact.
static long state_tenant_id_for_technician_id_runtime(yyjson_val *root, long technician_id)
static const unsigned char * memmem_runtime(const unsigned char *haystack, size_t haystack_len, const unsigned char *needle, size_t needle_len)
Finds the first binary needle occurrence within a byte range.
static char * dynamic_relational_runtime_export_json_dup_runtime(const char *app_root, const char *runtime_conf_path, const char *app_id)
Exports UCAL relational runtime state into canonical JSON using the configured backend contract.
static int handle_db_postgresql_audit_event_count_command(int argc, char **argv)
Emits the current PostgreSQL audit event count.
static int handle_template_password_reset_sent_page_json_command(int argc, char **argv)
static int handle_file_field_string_command(int argc, char **argv)
static int handle_validation_add_form_error_command(int argc, char **argv)
static int handle_dynamic_store_uploaded_media_command(int argc, char **argv)
Extracts one multipart upload into the app artifact media tree and returns the resulting media URL.
static int json_value_truthy_runtime(yyjson_val *value)
static yyjson_mut_val * ensure_mut_array_field_runtime(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *field_name)
static int load_native_libpq_api_runtime(NativeLibpqApi *api)
Loads the native libpq client ABI from cached image prefixes or standard library names.
static int generated_surface_text_is_decimal_runtime(const char *text)
static char * checksum_string_runtime(const char *value)
static int handle_generated_surface_form_fields_html_command(int argc, char **argv)
static int handle_db_sqlite_migration_applied_checksum_command(int argc, char **argv)
Emits one sqlite migration applied checksum.
static char * service_abbreviation_dup_runtime(const char *name)
static int append_generated_surface_relation_checkboxes_html_runtime(TextBufferRuntime *buffer, yyjson_val *state_root, yyjson_val *relation_root, yyjson_val *context_root, yyjson_val *selected_root)
static int handle_dynamic_postgresql_runtime_ready_command(int argc, char **argv)
Prepares the sqlite dynamic runtime schema, seed data, and migration ledger.
static int handle_generated_surface_manage_page_json_command(int argc, char **argv)
static int handle_db_sqlite_audit_event_count_command(int argc, char **argv)
Emits the current sqlite audit event count.
static yyjson_val * state_find_appointment_by_id_runtime(yyjson_val *root, long appointment_id)
static int emit_business_admin_technician_rows_json_runtime(yyjson_val *root, long business_id)
static int append_business_admin_service_runtime(business_admin_service_list_runtime *list, yyjson_val *service, long sort_order, long service_id)
static int emit_technician_availability_lines_runtime(yyjson_val *root, long technician_id, const char *day_name)
static int handle_dynamic_relational_runtime_export_json_command(int argc, char **argv)
Emits canonical exported JSON for a sqlite-backed dynamic relational runtime.
static int generated_surface_report_add_field_error_runtime(yyjson_mut_doc *doc, yyjson_mut_val *report_root, const char *field_id, const char *rule_id, const char *code_text, const char *message)
static int handle_validation_has_errors_command(int argc, char **argv)
static int handle_error_envelope_command(int argc, char **argv)
static int append_generated_surface_select_options_html_runtime(TextBufferRuntime *buffer, yyjson_val *field_root, const char *selected_value)
static char * dynamic_conf_lookup_runtime(const char *conf_path, const char *key)
Loads one raw config value from a path when the file exists.
static int handle_request_field_command(int argc, char **argv)
static int handle_template_invite_accept_page_json_command(int argc, char **argv)
static char * render_service_booking_options_editor_html_dup_runtime(yyjson_val *root, long service_id, const char *app_root)
static int route_template_match_into_mut_obj_runtime(yyjson_mut_doc *doc, const char *template_path, const char *request_path, yyjson_mut_val **out_params)
static int long_from_value_runtime(yyjson_val *value, long *out)
int handle_native_json_command(int argc, char **argv)
static yyjson_val * find_form_field_value_runtime(yyjson_val *root, const char *form_id, const char *field_id)
char * hidden_next_input_html_dup_runtime(const char *next_path)
Renders a hidden next-path <input> HTML, or an empty string.
static char * build_generated_surface_filter_controls_html_runtime(yyjson_val *surface_root, const char *raw_query)
static int template_path_reference_allowed_runtime(const char *text)
Tests whether one candidate path reference is safe for HTML URL attributes.
static int emit_managed_appointments_json_runtime(yyjson_val *root, yyjson_val *tenant_ids)
static int handle_object_path_array_length_command(int argc, char **argv)
static int emit_template_text_runtime(const char *template_text, yyjson_val *params_root)
static int emit_json_line_runtime(yyjson_val *value)
static int handle_generated_surface_coerce_candidate_command(int argc, char **argv)
char * apply_base_path_html_dup_runtime(const char *html, const char *base_path)
Rewrites relative URLs in HTML to use a base path.
static char * postgresql_conninfo_dup_runtime(const DynamicPostgresqlRuntimeConfig *config)
Builds one libpq conninfo string from merged runtime config values.
static int handle_template_render_file_command(int argc, char **argv)
static int handle_request_compact_json_command(int argc, char **argv)
static void * open_libpq_handle_candidate_runtime(const char *path)
Attempts to open one libpq shared library path.
static int emit_business_edit_page_json_runtime(yyjson_val *root, const char *slug)
static char * multipart_header_param_dup_runtime(const char *headers_text, const char *key)
Extracts one quoted Content-Disposition parameter from multipart headers.
static void free_native_libpq_api_runtime(NativeLibpqApi *api)
Releases one resolved native libpq ABI loader handle.
static yyjson_val * state_array_runtime(yyjson_val *root, const char *key)
static int emit_profile_page_json_runtime(yyjson_val *root, long user_id)
static int handle_generated_surface_validation_report_command(int argc, char **argv)
static int text_buffer_append_template_value_runtime(TextBufferRuntime *buffer, yyjson_val *value, TemplatePlaceholderKindRuntime kind)
Appends one template placeholder value using the requested safety mode.
static int handle_template_declared_fragment_path_command(int argc, char **argv)
Emits one UCAL declared fragment template path as text.
static int generated_surface_append_audit_event_mut_runtime(yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, yyjson_val *surface_root, const char *operation_name, yyjson_val *user_root, yyjson_val *context_root, yyjson_val *item_root, const char *created_at)
static int handle_request_compact_object_strict_command(int argc, char **argv)
static int emit_user_appointments_json_runtime(yyjson_val *root, long user_id)
static int handle_manifest_app_root_command(int argc, char **argv)
static int handle_generated_surface_default_candidate_command(int argc, char **argv)
static int handle_bundle_manifest_layer_for_slice_command(int argc, char **argv)
static int emit_business_availability_lines_runtime(yyjson_val *root, long business_id, const char *day_name)
static int emit_compact_mutable_json_doc_runtime(yyjson_mut_doc *doc)
static int handle_bundle_manifest_slice_order_command(int argc, char **argv)
static int surface_compare_sort_values_runtime(yyjson_val *left_item, yyjson_val *right_item, const char *field_name, const char *type_name)
static yyjson_val * migration_record_by_id_runtime(yyjson_val *root, const char *migration_id)
static int weekday_rank_runtime(const char *value)
static yyjson_val * state_find_user_by_email_runtime(yyjson_val *root, const char *email)
static int postgresql_apply_file_list_runtime(const DynamicPostgresqlRuntimeConfig *config, const char *app_root, yyjson_val *list)
Applies one declarative SQL file list through the native PostgreSQL driver.
static char * generated_surface_operation_path_dup_runtime(yyjson_val *surface_root, yyjson_val *context_root, const char *operation_name, const char *identity_value)
static int handle_db_postgresql_reconcile_migration_checksum_command(int argc, char **argv)
Updates one PostgreSQL migration checksum when the ledger row is missing one.
static int emit_object_string_field_runtime(yyjson_val *obj, const char *field_name, const char *default_value)
static int capture_postgresql_result_output_runtime(const NativeLibpqApi *api, PGresult *result, char **output_out)
Captures one tuple-producing PostgreSQL result into CLI-compatible line output.
static char * generated_surface_delete_strategy_runtime(yyjson_val *surface_root)
static int handle_template_render_path_command(int argc, char **argv)
static long event_stream_tenant_id_runtime(yyjson_val *root, yyjson_val *payload, long business_id)
static yyjson_val * state_find_user_by_username_runtime(yyjson_val *root, const char *username)
static int handle_object_field_string_command(int argc, char **argv)
static yyjson_mut_val * surface_build_sort_config_runtime(yyjson_mut_doc *doc, yyjson_val *surface_root, const char *raw_query)
static int handle_object_field_text_command(int argc, char **argv)
static int text_buffer_append_text_runtime(TextBufferRuntime *buffer, const char *text)
static int emit_profile_render_page_json_runtime(yyjson_val *root, long user_id, const char *app_root, const char *payment_preference_options_html)
Emits rendered profile-page template params using declarative subscription fragments.
static int emit_appointments_page_json_runtime(yyjson_val *root, long user_id, yyjson_val *tenant_ids, const char *app_root)
static int run_postgresql_sql_capture_runtime(const DynamicPostgresqlRuntimeConfig *config, const char *sql_text, int at_output, char **output_out)
Runs one PostgreSQL SQL text payload through the native libpq driver.
static int handle_dynamic_multipart_extract_file_command(int argc, char **argv)
Extracts a file from a multipart upload and writes it to disk.
static char * ucal_runtime_state_import_postgresql_path_dup_runtime(const char *app_root)
Resolves the declarative postgresql import query path for UCAL runtime-state bridge import.
static int emit_user_membership_tenant_ids_json_runtime(yyjson_val *root, long user_id)
static const char * service_badge_color_runtime(const char *name)
static int handle_file_normalize_ucal_state_command(int argc, char **argv)
static const char * ucal_declared_fragment_relative_path_runtime(const char *fragment_id)
Resolves one UCAL declared fragment id to its app-relative template path.
static int time_ranges_overlap_runtime(int start_a_minutes, int duration_a, int start_b_minutes, int duration_b)
static int handle_dynamic_media_disk_path_command(int argc, char **argv)
Emits the on-disk artifact path for a managed media URL.
static int handle_template_password_reset_complete_page_json_command(int argc, char **argv)
static int handle_db_sqlite_table_exists_command(int argc, char **argv)
Emits whether one sqlite table exists.
static int template_reference_has_allowed_url_scheme_runtime(const char *text)
Tests whether a reference uses one permitted external URL scheme.
static int conninfo_append_kv_runtime(TextBufferRuntime *buffer, const char *key, const char *value)
Appends one libpq conninfo key value pair.
static yyjson_val * state_find_service_by_id_runtime(yyjson_val *root, long service_id)
static char * build_generated_surface_form_fields_html_runtime(yyjson_val *state_root, yyjson_val *fixture_root, yyjson_val *surface_root, yyjson_val *context_root, yyjson_val *item_root)
static int handle_object_path_json_command(int argc, char **argv)
static void json_value_list_free_runtime(json_value_list_runtime *list)
static int handle_form_field_json_command(int argc, char **argv)
TemplatePlaceholderKindRuntime
@ TEMPLATE_PLACEHOLDER_PATH_RUNTIME
@ TEMPLATE_PLACEHOLDER_URL_RUNTIME
@ TEMPLATE_PLACEHOLDER_HTML_RUNTIME
@ TEMPLATE_PLACEHOLDER_TEXT_RUNTIME
static int handle_request_form_field_command(int argc, char **argv)
Returns one request form field value from urlencoded or multipart request inputs.
static int emit_business_admin_page_json_runtime(yyjson_val *root, const char *slug, const char *app_root, const char *invite_account_type_options_html)
static int long_in_array_runtime(yyjson_val *array_value, long expected_value)
static char * query_value_decoded_dup_runtime(const char *raw_query, const char *key)
static int handle_validation_summary_text_command(int argc, char **argv)
static int handle_validation_empty_report_command(int argc, char **argv)
static yyjson_val * state_find_business_availability_for_business_runtime(yyjson_val *root, long availability_id, long business_id)
static yyjson_val * state_find_service_for_business_runtime(yyjson_val *root, long service_id, long business_id)
static long tenant_id_for_business_runtime(yyjson_val *root, long business_id)
static char * dup_or_default_runtime(const char *text, const char *fallback)
static int handle_generated_surface_display_value_command(int argc, char **argv)
static yyjson_val * surface_find_by_id_runtime(yyjson_val *root, const char *surface_id)
static int handle_file_migration_backend_record_json_command(int argc, char **argv)
static yyjson_val * state_find_business_by_id_runtime(yyjson_val *root, long business_id)
static int handle_form_field_json_lines_command(int argc, char **argv)
static int handle_generated_surface_submitted_command(int argc, char **argv)
static int emit_service_assigned_technician_ids_lines_runtime(yyjson_val *root, yyjson_val *service, long business_id)
static int handle_form_options_html_command(int argc, char **argv)
static int text_buffer_append_char_runtime(TextBufferRuntime *buffer, char value)
static int handle_dynamic_file_signature_command(int argc, char **argv)
Emits a stable file signature for change detection.
static int handle_file_migration_backend_list_lines_command(int argc, char **argv)
static int generated_surface_text_is_date_runtime(const char *text)
static char * payload_first_string_dup_runtime(yyjson_val *payload, const char **fields, size_t field_count)
static char * html_escape_dup_runtime(const char *text)
static int append_event_stream_row_runtime(event_stream_row_list_runtime *list, event_stream_row_runtime *row)
static int handle_db_postgresql_table_row_count_command(int argc, char **argv)
Emits one PostgreSQL public table row count.
static int handle_db_postgresql_create_database_if_missing_command(int argc, char **argv)
Creates one PostgreSQL database when it is missing.
static int compare_event_stream_rows_runtime(const void *lhs_ptr, const void *rhs_ptr)
static int handle_request_booking_options_json_from_form_command(int argc, char **argv)
Extracts booking option rows directly from repeated request form fields.
static int contains_casefold_runtime(const char *haystack, const char *needle)
static int handle_object_array_item_json_lines_command(int argc, char **argv)
char * render_page_html_dup_runtime(const char *app_root, const char *base_path, const char *layout_id, const char *title, const char *body, const char *current_lang, const char *current_request_target, int has_user, int managed_business_count)
Builds the nav context and renders a full layout page.
static int emit_json_or_empty_runtime(yyjson_val *value)
static int load_dynamic_postgresql_runtime_config(const char *app_root, const char *runtime_conf_path, DynamicPostgresqlRuntimeConfig *config)
Loads merged PostgreSQL runtime connection settings for one dynamic app.
static int conninfo_append_text_runtime(TextBufferRuntime *buffer, const char *text, int escape_quotes)
Appends one libpq conninfo key or value fragment with native escaping.
static char * ucal_runtime_state_import_sqlite_path_dup_runtime(const char *app_root)
Resolves the declarative sqlite import query path for UCAL runtime-state bridge import.
static int handle_request_compact_object_command(int argc, char **argv)
static long assign_technician_for_slot_runtime(yyjson_val *root, long business_id, long service_id, const char *date_value, const char *time_value)
static char * render_business_fragment_from_doc_dup_runtime(const char *app_root, const char *fragment_name, yyjson_mut_doc *doc)
static yyjson_val * surface_find_form_runtime(yyjson_val *root, const char *form_id)
static int emit_service_book_form_page_json_runtime(yyjson_val *root, const char *slug, long service_id, const char *app_root, const char *error_message)
static int handle_db_postgresql_migration_applied_checksum_command(int argc, char **argv)
Emits one PostgreSQL migration applied checksum.
static int handle_request_checkbox_id_array_json_command(int argc, char **argv)
static int handle_db_postgresql_database_exists_command(int argc, char **argv)
Emits whether one PostgreSQL database exists.
static void free_dynamic_postgresql_runtime_config(DynamicPostgresqlRuntimeConfig *config)
Releases owned PostgreSQL runtime connection settings.
static int sqlite_capture_callback_runtime(void *context_void, int column_count, char **column_values, char **column_names)
Captures sqlite result rows into the shared text buffer using CLI-compatible separators.
static int collect_event_stream_row_runtime(event_stream_row_list_runtime *rows, yyjson_val *root, const char *source, const char *event_kind_default, yyjson_val *payload, const char *created_at_field_fallback, long native_order, yyjson_val *subscription_scope, yyjson_val *filters, const char *created_at_override, const char *event_kind_override)
static int text_buffer_append_format_runtime(TextBufferRuntime *buffer, const char *format,...)
static yyjson_val * state_find_technician_by_id_runtime(yyjson_val *root, long technician_id)
static int handle_object_array_all_integers_command(int argc, char **argv)
static int user_manages_business_runtime(yyjson_val *root, long user_id, long business_id)
static int dynamic_config_key_is_path_runtime(const char *key)
Returns non-zero when a dynamic config key should resolve relative to its owning config file.
static int handle_db_sqlite_reconcile_migration_checksum_command(int argc, char **argv)
Updates a sqlite migration checksum when the ledger row is missing one.
static int emit_active_tenant_ids_lines_runtime(yyjson_val *root)
static int generated_surface_normalize_order_mut_runtime(yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, yyjson_val *surface_root, yyjson_val *context_root)
static int identifier_safe_runtime(const char *text)
Tests whether one identifier contains only letters, digits, and underscores.
static int handle_request_form_value_command(int argc, char **argv)
Extracts a value from a URL-encoded form body.
int(* NativeJsonCommandHandlerFn)(int argc, char **argv)
static int compare_generated_surface_order_item_runtime(const void *left_ptr, const void *right_ptr)
static int append_date_runtime(TextBufferRuntime *buffer, int year, int month, int day)
static char * business_media_url_dup_runtime(const char *business_slug, const char *media_url)
int write_binary_file_runtime(const char *path, const unsigned char *data, size_t length)
Writes one binary byte range to a file path, creating parent directories as needed.
static int handle_generated_surface_context_command(int argc, char **argv)
static int emit_business_calendar_render_page_json_runtime(yyjson_val *root, const char *slug, const char *app_root, const char *selected_year_text, const char *selected_month_text, const char *selected_date_text)
static long state_tenant_id_for_service_id_runtime(yyjson_val *root, long service_id)
static yyjson_mut_val * normalized_payload_object_mut_runtime(yyjson_mut_doc *doc, const char *payload_json)
static yyjson_val * state_find_booking_for_user_runtime(yyjson_val *root, long booking_id, long user_id)
static int write_mutable_json_doc_runtime(yyjson_mut_doc *doc, const char *path)
static void free_event_stream_row_runtime(event_stream_row_runtime *row)
static int handle_db_postgresql_migration_applied_schema_version_command(int argc, char **argv)
Emits one PostgreSQL migration applied schema version.
static int handle_file_section_length_command(int argc, char **argv)
static char * event_stream_resource_id_dup_runtime(yyjson_val *payload)
static int emit_public_business_cards_json_runtime(yyjson_val *root)
static int handle_db_sqlite_query_value_command(int argc, char **argv)
Emits the first-line output of one sqlite query.
char * diagnostics_render_page_params_json_dup_runtime(const char *state_path)
static int append_validation_error_runtime(const char *report_json, const char *array_name, const char *field_id, const char *rule_id, const char *code_text, const char *message)
static int handle_manifest_fixture_path_raw_command(int argc, char **argv)
yyjson_val * load_root_from_text_runtime(const char *text, yyjson_doc **doc_out)
Parses JSON text into a yyjson_val root.
static int emit_manageable_business_cards_json_runtime(yyjson_val *root, yyjson_val *tenant_ids)
static void text_buffer_free_runtime(TextBufferRuntime *buffer)
static const char * month_name_runtime(int month)
static int handle_object_array_lines_command(int argc, char **argv)
static int handle_object_array_length_command(int argc, char **argv)
static int generated_surface_text_is_time_runtime(const char *text)
static int handle_request_cookie_value_command(int argc, char **argv)
Extracts a cookie value from a Cookie header string.
static int handle_dynamic_relational_runtime_import_state_command(int argc, char **argv)
Imports canonical state into a sqlite-backed dynamic relational runtime.
int capture_execv_output_native(char *const argv[], char **output, size_t *output_len)
Executes a process and captures its combined output.
Declares child-process capture helpers.
char * path_join(const char *left, const char *right)
Joins two path segments using the native path separator.
void trim_in_place(char *text)
Trims leading and trailing ASCII whitespace from a string in place.
char * dup_range(const char *start, size_t len)
Duplicates a byte range into a newly allocated NUL-terminated string.
int streq(const char *a, const char *b)
Tests whether two strings are exactly equal.
char * path_dirname(const char *path)
Returns the parent directory component of a path.
char * resolve_relative_to(const char *base_dir, const char *value)
Resolves a path value relative to a base directory when needed.
char * read_file_text(const char *path)
Reads an entire file into a newly allocated text buffer.
int path_exists(const char *path)
Tests whether a filesystem path currently exists.
Declares shared low-level support helpers used across the Pharos native runtime.
yyjson_doc * native_json_doc_load_file(const char *path)
Parses a JSON file into an owned yyjson document.
long native_json_obj_get_long_default(yyjson_val *obj, const char *key, long default_value, int *found_out)
Reads a numeric object member or returns a fallback value.
void native_json_doc_free(yyjson_doc *doc)
Releases a document returned by the native yyjson helpers.
yyjson_doc * native_json_doc_load_text(const char *text)
Parses JSON text into an owned yyjson document.
char * native_json_obj_get_string_dup(yyjson_val *obj, const char *key)
Duplicates a string-valued key from a JSON object.
yyjson_val * native_json_obj_get_array(yyjson_val *obj, const char *key)
Looks up an array-valued key from a JSON object value.
yyjson_val * native_json_obj_get(yyjson_val *obj, const char *key)
Looks up a key from a JSON object value.
char * native_json_serialize_compact_dup(yyjson_val *value)
Serializes a JSON value to compact JSON text.
Declares shared helper wrappers around vendored yyjson parsing and serialization APIs.
Carries resolved PostgreSQL runtime connection settings for native relational bridge commands.
NativeJsonCommandHandlerFn handler
int argv_offset
const char * command_name
const char * group_name
char *(* PQerrorMessage)(const PGconn *conn)
PGconn *(* PQconnectdb)(const char *conninfo)
char *(* PQgetvalue)(const PGresult *res, int row_number, int column_number)
int(* PQresultStatus)(const PGresult *res)
int(* PQntuples)(const PGresult *res)
int(* PQstatus)(const PGconn *conn)
void(* PQclear)(PGresult *res)
void(* PQfinish)(PGconn *conn)
PGresult *(* PQexec)(PGconn *conn, const char *query)
int(* PQnfields)(const PGresult *res)
business_admin_service_runtime * items
void yyjson_mut_doc_free(yyjson_mut_doc *doc)
Definition yyjson.c:2653
yyjson_mut_doc * yyjson_mut_doc_new(const yyjson_alc *alc)
Definition yyjson.c:2663
yyjson_mut_doc * yyjson_doc_mut_copy(const yyjson_doc *doc, const yyjson_alc *alc)
Definition yyjson.c:2678
yyjson_mut_val * yyjson_val_mut_copy(yyjson_mut_doc *m_doc, const yyjson_val *i_vals)
Definition yyjson.c:2714
yyjson_api_inline size_t yyjson_mut_arr_size(const yyjson_mut_val *arr)
Definition yyjson.h:6314
yyjson_api_inline yyjson_mut_val * yyjson_mut_obj_remove_key(yyjson_mut_val *obj, const char *key)
Definition yyjson.h:7316
yyjson_api_inline bool yyjson_is_str(const yyjson_val *val)
Definition yyjson.h:5390
yyjson_api_inline yyjson_mut_val * yyjson_mut_strncpy(yyjson_mut_doc *doc, const char *str, size_t len)
Definition yyjson.h:6295
yyjson_api_inline bool yyjson_is_obj(const yyjson_val *val)
Definition yyjson.h:5398
yyjson_api_inline bool yyjson_mut_obj_add_val(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, yyjson_mut_val *val)
Definition yyjson.h:7521
#define yyjson_mut_arr_foreach(arr, idx, max, val)
Definition yyjson.h:2987
yyjson_api_inline yyjson_mut_val * yyjson_mut_arr_remove(yyjson_mut_val *arr, size_t idx)
Definition yyjson.h:6687
yyjson_api_inline bool yyjson_is_sint(const yyjson_val *val)
Definition yyjson.h:5374
yyjson_api_inline yyjson_mut_val * yyjson_mut_obj_get(const yyjson_mut_val *obj, const char *key)
Definition yyjson.h:6970
yyjson_api_inline uint64_t yyjson_get_uint(const yyjson_val *val)
Definition yyjson.h:5449
yyjson_api_inline yyjson_val * yyjson_obj_iter_get_val(yyjson_val *key)
Definition yyjson.h:5751
yyjson_api_inline int64_t yyjson_get_sint(const yyjson_val *val)
Definition yyjson.h:5453
yyjson_api_inline yyjson_val * yyjson_arr_get_first(const yyjson_val *arr)
Definition yyjson.h:5619
yyjson_api_inline yyjson_val * yyjson_arr_get(const yyjson_val *arr, size_t idx)
Definition yyjson.h:5603
yyjson_api_inline bool yyjson_mut_obj_add_int(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, int64_t val)
Definition yyjson.h:7425
yyjson_api_inline yyjson_mut_val * yyjson_mut_bool(yyjson_mut_doc *doc, bool val)
Definition yyjson.h:6236
yyjson_api_inline yyjson_val * yyjson_obj_iter_next(yyjson_obj_iter *iter)
Definition yyjson.h:5741
yyjson_api_inline yyjson_val * yyjson_doc_get_root(const yyjson_doc *doc)
Definition yyjson.h:5323
yyjson_api_inline bool yyjson_mut_obj_add_null(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key)
Definition yyjson.h:7386
yyjson_api_inline bool yyjson_is_arr(const yyjson_val *val)
Definition yyjson.h:5394
yyjson_api_inline char * yyjson_mut_val_write(const yyjson_mut_val *val, yyjson_write_flag flg, size_t *len)
Definition yyjson.h:1869
yyjson_api_inline yyjson_mut_val * yyjson_mut_obj(yyjson_mut_doc *doc)
Definition yyjson.h:7086
yyjson_api_inline yyjson_mut_val * yyjson_mut_null(yyjson_mut_doc *doc)
Definition yyjson.h:6224
yyjson_api_inline bool yyjson_is_int(const yyjson_val *val)
Definition yyjson.h:5378
yyjson_api_inline bool yyjson_is_real(const yyjson_val *val)
Definition yyjson.h:5382
yyjson_api_inline bool yyjson_obj_iter_init(const yyjson_val *obj, yyjson_obj_iter *iter)
Definition yyjson.h:5718
yyjson_api_inline char * yyjson_mut_write(const yyjson_mut_doc *doc, yyjson_write_flag flg, size_t *len)
Definition yyjson.h:1604
yyjson_api_inline void yyjson_mut_doc_set_root(yyjson_mut_doc *doc, yyjson_mut_val *root)
Definition yyjson.h:5919
yyjson_api_inline yyjson_mut_val * yyjson_mut_int(yyjson_mut_doc *doc, int64_t num)
Definition yyjson.h:6251
yyjson_api_inline yyjson_mut_val * yyjson_mut_strcpy(yyjson_mut_doc *doc, const char *str)
Definition yyjson.h:6282
yyjson_api_inline bool yyjson_equals_str(const yyjson_val *val, const char *str)
Definition yyjson.h:5477
yyjson_api_inline bool yyjson_mut_is_obj(const yyjson_mut_val *val)
Definition yyjson.h:5978
yyjson_api_inline yyjson_mut_val * yyjson_mut_doc_get_root(yyjson_mut_doc *doc)
Definition yyjson.h:5915
yyjson_api_inline bool yyjson_is_uint(const yyjson_val *val)
Definition yyjson.h:5370
yyjson_api_inline yyjson_val * yyjson_arr_iter_next(yyjson_arr_iter *iter)
Definition yyjson.h:5672
yyjson_api_inline bool yyjson_is_num(const yyjson_val *val)
Definition yyjson.h:5386
yyjson_api_inline bool yyjson_is_null(const yyjson_val *val)
Definition yyjson.h:5354
yyjson_api_inline yyjson_mut_val * yyjson_mut_arr_get(const yyjson_mut_val *arr, size_t idx)
Definition yyjson.h:6318
yyjson_api_inline size_t yyjson_get_len(const yyjson_val *val)
Definition yyjson.h:5473
yyjson_api_inline bool yyjson_mut_obj_add_bool(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, bool val)
Definition yyjson.h:7404
yyjson_api_inline bool yyjson_mut_obj_add_strcpy(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, const char *val)
Definition yyjson.h:7479
yyjson_api_inline bool yyjson_mut_obj_add(yyjson_mut_val *obj, yyjson_mut_val *key, yyjson_mut_val *val)
Definition yyjson.h:7248
const char size_t len
Definition yyjson.h:8364
yyjson_api_inline size_t yyjson_arr_size(const yyjson_val *arr)
Definition yyjson.h:5599
yyjson_api_inline bool yyjson_mut_obj_put(yyjson_mut_val *obj, yyjson_mut_val *key, yyjson_mut_val *val)
Definition yyjson.h:7259
static const yyjson_write_flag YYJSON_WRITE_NOFLAG
Definition yyjson.h:1245
yyjson_api_inline const char * yyjson_get_str(const yyjson_val *val)
Definition yyjson.h:5469
yyjson_api_inline double yyjson_get_real(const yyjson_val *val)
Definition yyjson.h:5461
yyjson_api_inline bool yyjson_mut_is_arr(const yyjson_mut_val *val)
Definition yyjson.h:5974
yyjson_api_inline yyjson_mut_val * yyjson_mut_arr(yyjson_mut_doc *doc)
Definition yyjson.h:6410
yyjson_api_inline bool yyjson_mut_arr_append(yyjson_mut_val *arr, yyjson_mut_val *val)
Definition yyjson.h:6621
yyjson_api_inline bool yyjson_arr_iter_init(const yyjson_val *arr, yyjson_arr_iter *iter)
Definition yyjson.h:5650
yyjson_api_inline bool yyjson_get_bool(const yyjson_val *val)
Definition yyjson.h:5445
yyjson_api_inline bool yyjson_is_bool(const yyjson_val *val)
Definition yyjson.h:5366