kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add user-based Gotify notifications

Tin 28bcfcfd 16268fe1

+3273
+4
apps/api/drizzle/0021_tiny_bill_hollister.sql
··· 1 + ALTER TABLE "user_notification_preference" ADD COLUMN "gotify_enabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint 2 + ALTER TABLE "user_notification_preference" ADD COLUMN "gotify_server_url" text;--> statement-breakpoint 3 + ALTER TABLE "user_notification_preference" ADD COLUMN "gotify_token" text;--> statement-breakpoint 4 + ALTER TABLE "user_notification_workspace_rule" ADD COLUMN "gotify_enabled" boolean DEFAULT false NOT NULL;
+2880
apps/api/drizzle/meta/0021_snapshot.json
··· 1 + { 2 + "id": "0bc097ca-a6c6-4555-930d-a15f045eb3e8", 3 + "prevId": "745512c9-3ace-4437-abaf-697e303b6d2a", 4 + "version": "7", 5 + "dialect": "postgresql", 6 + "tables": { 7 + "public.account": { 8 + "name": "account", 9 + "schema": "", 10 + "columns": { 11 + "id": { 12 + "name": "id", 13 + "type": "text", 14 + "primaryKey": true, 15 + "notNull": true 16 + }, 17 + "account_id": { 18 + "name": "account_id", 19 + "type": "text", 20 + "primaryKey": false, 21 + "notNull": true 22 + }, 23 + "provider_id": { 24 + "name": "provider_id", 25 + "type": "text", 26 + "primaryKey": false, 27 + "notNull": true 28 + }, 29 + "user_id": { 30 + "name": "user_id", 31 + "type": "text", 32 + "primaryKey": false, 33 + "notNull": true 34 + }, 35 + "access_token": { 36 + "name": "access_token", 37 + "type": "text", 38 + "primaryKey": false, 39 + "notNull": false 40 + }, 41 + "refresh_token": { 42 + "name": "refresh_token", 43 + "type": "text", 44 + "primaryKey": false, 45 + "notNull": false 46 + }, 47 + "id_token": { 48 + "name": "id_token", 49 + "type": "text", 50 + "primaryKey": false, 51 + "notNull": false 52 + }, 53 + "access_token_expires_at": { 54 + "name": "access_token_expires_at", 55 + "type": "timestamp", 56 + "primaryKey": false, 57 + "notNull": false 58 + }, 59 + "refresh_token_expires_at": { 60 + "name": "refresh_token_expires_at", 61 + "type": "timestamp", 62 + "primaryKey": false, 63 + "notNull": false 64 + }, 65 + "scope": { 66 + "name": "scope", 67 + "type": "text", 68 + "primaryKey": false, 69 + "notNull": false 70 + }, 71 + "password": { 72 + "name": "password", 73 + "type": "text", 74 + "primaryKey": false, 75 + "notNull": false 76 + }, 77 + "created_at": { 78 + "name": "created_at", 79 + "type": "timestamp", 80 + "primaryKey": false, 81 + "notNull": true, 82 + "default": "now()" 83 + }, 84 + "updated_at": { 85 + "name": "updated_at", 86 + "type": "timestamp", 87 + "primaryKey": false, 88 + "notNull": true 89 + } 90 + }, 91 + "indexes": { 92 + "account_userId_idx": { 93 + "name": "account_userId_idx", 94 + "columns": [ 95 + { 96 + "expression": "user_id", 97 + "isExpression": false, 98 + "asc": true, 99 + "nulls": "last" 100 + } 101 + ], 102 + "isUnique": false, 103 + "concurrently": false, 104 + "method": "btree", 105 + "with": {} 106 + } 107 + }, 108 + "foreignKeys": { 109 + "account_user_id_user_id_fk": { 110 + "name": "account_user_id_user_id_fk", 111 + "tableFrom": "account", 112 + "tableTo": "user", 113 + "columnsFrom": ["user_id"], 114 + "columnsTo": ["id"], 115 + "onDelete": "cascade", 116 + "onUpdate": "no action" 117 + } 118 + }, 119 + "compositePrimaryKeys": {}, 120 + "uniqueConstraints": {}, 121 + "policies": {}, 122 + "checkConstraints": {}, 123 + "isRLSEnabled": false 124 + }, 125 + "public.activity": { 126 + "name": "activity", 127 + "schema": "", 128 + "columns": { 129 + "id": { 130 + "name": "id", 131 + "type": "text", 132 + "primaryKey": true, 133 + "notNull": true 134 + }, 135 + "task_id": { 136 + "name": "task_id", 137 + "type": "text", 138 + "primaryKey": false, 139 + "notNull": true 140 + }, 141 + "type": { 142 + "name": "type", 143 + "type": "text", 144 + "primaryKey": false, 145 + "notNull": true 146 + }, 147 + "created_at": { 148 + "name": "created_at", 149 + "type": "timestamp", 150 + "primaryKey": false, 151 + "notNull": true, 152 + "default": "now()" 153 + }, 154 + "user_id": { 155 + "name": "user_id", 156 + "type": "text", 157 + "primaryKey": false, 158 + "notNull": false 159 + }, 160 + "content": { 161 + "name": "content", 162 + "type": "text", 163 + "primaryKey": false, 164 + "notNull": false 165 + }, 166 + "event_data": { 167 + "name": "event_data", 168 + "type": "jsonb", 169 + "primaryKey": false, 170 + "notNull": false 171 + }, 172 + "external_user_name": { 173 + "name": "external_user_name", 174 + "type": "text", 175 + "primaryKey": false, 176 + "notNull": false 177 + }, 178 + "external_user_avatar": { 179 + "name": "external_user_avatar", 180 + "type": "text", 181 + "primaryKey": false, 182 + "notNull": false 183 + }, 184 + "external_source": { 185 + "name": "external_source", 186 + "type": "text", 187 + "primaryKey": false, 188 + "notNull": false 189 + }, 190 + "external_url": { 191 + "name": "external_url", 192 + "type": "text", 193 + "primaryKey": false, 194 + "notNull": false 195 + } 196 + }, 197 + "indexes": {}, 198 + "foreignKeys": { 199 + "activity_task_id_task_id_fk": { 200 + "name": "activity_task_id_task_id_fk", 201 + "tableFrom": "activity", 202 + "tableTo": "task", 203 + "columnsFrom": ["task_id"], 204 + "columnsTo": ["id"], 205 + "onDelete": "cascade", 206 + "onUpdate": "cascade" 207 + }, 208 + "activity_user_id_user_id_fk": { 209 + "name": "activity_user_id_user_id_fk", 210 + "tableFrom": "activity", 211 + "tableTo": "user", 212 + "columnsFrom": ["user_id"], 213 + "columnsTo": ["id"], 214 + "onDelete": "cascade", 215 + "onUpdate": "cascade" 216 + } 217 + }, 218 + "compositePrimaryKeys": {}, 219 + "uniqueConstraints": {}, 220 + "policies": {}, 221 + "checkConstraints": {}, 222 + "isRLSEnabled": false 223 + }, 224 + "public.apikey": { 225 + "name": "apikey", 226 + "schema": "", 227 + "columns": { 228 + "id": { 229 + "name": "id", 230 + "type": "text", 231 + "primaryKey": true, 232 + "notNull": true 233 + }, 234 + "config_id": { 235 + "name": "config_id", 236 + "type": "text", 237 + "primaryKey": false, 238 + "notNull": true, 239 + "default": "'default'" 240 + }, 241 + "name": { 242 + "name": "name", 243 + "type": "text", 244 + "primaryKey": false, 245 + "notNull": false 246 + }, 247 + "start": { 248 + "name": "start", 249 + "type": "text", 250 + "primaryKey": false, 251 + "notNull": false 252 + }, 253 + "reference_id": { 254 + "name": "reference_id", 255 + "type": "text", 256 + "primaryKey": false, 257 + "notNull": true 258 + }, 259 + "prefix": { 260 + "name": "prefix", 261 + "type": "text", 262 + "primaryKey": false, 263 + "notNull": false 264 + }, 265 + "key": { 266 + "name": "key", 267 + "type": "text", 268 + "primaryKey": false, 269 + "notNull": true 270 + }, 271 + "user_id": { 272 + "name": "user_id", 273 + "type": "text", 274 + "primaryKey": false, 275 + "notNull": false 276 + }, 277 + "refill_interval": { 278 + "name": "refill_interval", 279 + "type": "integer", 280 + "primaryKey": false, 281 + "notNull": false 282 + }, 283 + "refill_amount": { 284 + "name": "refill_amount", 285 + "type": "integer", 286 + "primaryKey": false, 287 + "notNull": false 288 + }, 289 + "last_refill_at": { 290 + "name": "last_refill_at", 291 + "type": "timestamp", 292 + "primaryKey": false, 293 + "notNull": false 294 + }, 295 + "enabled": { 296 + "name": "enabled", 297 + "type": "boolean", 298 + "primaryKey": false, 299 + "notNull": false, 300 + "default": true 301 + }, 302 + "rate_limit_enabled": { 303 + "name": "rate_limit_enabled", 304 + "type": "boolean", 305 + "primaryKey": false, 306 + "notNull": false, 307 + "default": true 308 + }, 309 + "rate_limit_time_window": { 310 + "name": "rate_limit_time_window", 311 + "type": "integer", 312 + "primaryKey": false, 313 + "notNull": false, 314 + "default": 86400000 315 + }, 316 + "rate_limit_max": { 317 + "name": "rate_limit_max", 318 + "type": "integer", 319 + "primaryKey": false, 320 + "notNull": false, 321 + "default": 10 322 + }, 323 + "request_count": { 324 + "name": "request_count", 325 + "type": "integer", 326 + "primaryKey": false, 327 + "notNull": false, 328 + "default": 0 329 + }, 330 + "remaining": { 331 + "name": "remaining", 332 + "type": "integer", 333 + "primaryKey": false, 334 + "notNull": false 335 + }, 336 + "last_request": { 337 + "name": "last_request", 338 + "type": "timestamp", 339 + "primaryKey": false, 340 + "notNull": false 341 + }, 342 + "expires_at": { 343 + "name": "expires_at", 344 + "type": "timestamp", 345 + "primaryKey": false, 346 + "notNull": false 347 + }, 348 + "created_at": { 349 + "name": "created_at", 350 + "type": "timestamp", 351 + "primaryKey": false, 352 + "notNull": true 353 + }, 354 + "updated_at": { 355 + "name": "updated_at", 356 + "type": "timestamp", 357 + "primaryKey": false, 358 + "notNull": true 359 + }, 360 + "permissions": { 361 + "name": "permissions", 362 + "type": "text", 363 + "primaryKey": false, 364 + "notNull": false 365 + }, 366 + "metadata": { 367 + "name": "metadata", 368 + "type": "text", 369 + "primaryKey": false, 370 + "notNull": false 371 + } 372 + }, 373 + "indexes": { 374 + "apikey_configId_idx": { 375 + "name": "apikey_configId_idx", 376 + "columns": [ 377 + { 378 + "expression": "config_id", 379 + "isExpression": false, 380 + "asc": true, 381 + "nulls": "last" 382 + } 383 + ], 384 + "isUnique": false, 385 + "concurrently": false, 386 + "method": "btree", 387 + "with": {} 388 + }, 389 + "apikey_key_idx": { 390 + "name": "apikey_key_idx", 391 + "columns": [ 392 + { 393 + "expression": "key", 394 + "isExpression": false, 395 + "asc": true, 396 + "nulls": "last" 397 + } 398 + ], 399 + "isUnique": false, 400 + "concurrently": false, 401 + "method": "btree", 402 + "with": {} 403 + }, 404 + "apikey_referenceId_idx": { 405 + "name": "apikey_referenceId_idx", 406 + "columns": [ 407 + { 408 + "expression": "reference_id", 409 + "isExpression": false, 410 + "asc": true, 411 + "nulls": "last" 412 + } 413 + ], 414 + "isUnique": false, 415 + "concurrently": false, 416 + "method": "btree", 417 + "with": {} 418 + }, 419 + "apikey_userId_idx": { 420 + "name": "apikey_userId_idx", 421 + "columns": [ 422 + { 423 + "expression": "user_id", 424 + "isExpression": false, 425 + "asc": true, 426 + "nulls": "last" 427 + } 428 + ], 429 + "isUnique": false, 430 + "concurrently": false, 431 + "method": "btree", 432 + "with": {} 433 + } 434 + }, 435 + "foreignKeys": { 436 + "apikey_reference_id_user_id_fk": { 437 + "name": "apikey_reference_id_user_id_fk", 438 + "tableFrom": "apikey", 439 + "tableTo": "user", 440 + "columnsFrom": ["reference_id"], 441 + "columnsTo": ["id"], 442 + "onDelete": "cascade", 443 + "onUpdate": "no action" 444 + }, 445 + "apikey_user_id_user_id_fk": { 446 + "name": "apikey_user_id_user_id_fk", 447 + "tableFrom": "apikey", 448 + "tableTo": "user", 449 + "columnsFrom": ["user_id"], 450 + "columnsTo": ["id"], 451 + "onDelete": "cascade", 452 + "onUpdate": "no action" 453 + } 454 + }, 455 + "compositePrimaryKeys": {}, 456 + "uniqueConstraints": {}, 457 + "policies": {}, 458 + "checkConstraints": {}, 459 + "isRLSEnabled": false 460 + }, 461 + "public.asset": { 462 + "name": "asset", 463 + "schema": "", 464 + "columns": { 465 + "id": { 466 + "name": "id", 467 + "type": "text", 468 + "primaryKey": true, 469 + "notNull": true 470 + }, 471 + "workspace_id": { 472 + "name": "workspace_id", 473 + "type": "text", 474 + "primaryKey": false, 475 + "notNull": true 476 + }, 477 + "project_id": { 478 + "name": "project_id", 479 + "type": "text", 480 + "primaryKey": false, 481 + "notNull": true 482 + }, 483 + "task_id": { 484 + "name": "task_id", 485 + "type": "text", 486 + "primaryKey": false, 487 + "notNull": false 488 + }, 489 + "activity_id": { 490 + "name": "activity_id", 491 + "type": "text", 492 + "primaryKey": false, 493 + "notNull": false 494 + }, 495 + "object_key": { 496 + "name": "object_key", 497 + "type": "text", 498 + "primaryKey": false, 499 + "notNull": true 500 + }, 501 + "filename": { 502 + "name": "filename", 503 + "type": "text", 504 + "primaryKey": false, 505 + "notNull": true 506 + }, 507 + "mime_type": { 508 + "name": "mime_type", 509 + "type": "text", 510 + "primaryKey": false, 511 + "notNull": true 512 + }, 513 + "size": { 514 + "name": "size", 515 + "type": "integer", 516 + "primaryKey": false, 517 + "notNull": true 518 + }, 519 + "kind": { 520 + "name": "kind", 521 + "type": "text", 522 + "primaryKey": false, 523 + "notNull": true, 524 + "default": "'image'" 525 + }, 526 + "surface": { 527 + "name": "surface", 528 + "type": "text", 529 + "primaryKey": false, 530 + "notNull": true, 531 + "default": "'description'" 532 + }, 533 + "created_by": { 534 + "name": "created_by", 535 + "type": "text", 536 + "primaryKey": false, 537 + "notNull": false 538 + }, 539 + "created_at": { 540 + "name": "created_at", 541 + "type": "timestamp", 542 + "primaryKey": false, 543 + "notNull": true, 544 + "default": "now()" 545 + } 546 + }, 547 + "indexes": { 548 + "asset_workspaceId_idx": { 549 + "name": "asset_workspaceId_idx", 550 + "columns": [ 551 + { 552 + "expression": "workspace_id", 553 + "isExpression": false, 554 + "asc": true, 555 + "nulls": "last" 556 + } 557 + ], 558 + "isUnique": false, 559 + "concurrently": false, 560 + "method": "btree", 561 + "with": {} 562 + }, 563 + "asset_projectId_idx": { 564 + "name": "asset_projectId_idx", 565 + "columns": [ 566 + { 567 + "expression": "project_id", 568 + "isExpression": false, 569 + "asc": true, 570 + "nulls": "last" 571 + } 572 + ], 573 + "isUnique": false, 574 + "concurrently": false, 575 + "method": "btree", 576 + "with": {} 577 + }, 578 + "asset_taskId_idx": { 579 + "name": "asset_taskId_idx", 580 + "columns": [ 581 + { 582 + "expression": "task_id", 583 + "isExpression": false, 584 + "asc": true, 585 + "nulls": "last" 586 + } 587 + ], 588 + "isUnique": false, 589 + "concurrently": false, 590 + "method": "btree", 591 + "with": {} 592 + }, 593 + "asset_activityId_idx": { 594 + "name": "asset_activityId_idx", 595 + "columns": [ 596 + { 597 + "expression": "activity_id", 598 + "isExpression": false, 599 + "asc": true, 600 + "nulls": "last" 601 + } 602 + ], 603 + "isUnique": false, 604 + "concurrently": false, 605 + "method": "btree", 606 + "with": {} 607 + } 608 + }, 609 + "foreignKeys": { 610 + "asset_workspace_id_workspace_id_fk": { 611 + "name": "asset_workspace_id_workspace_id_fk", 612 + "tableFrom": "asset", 613 + "tableTo": "workspace", 614 + "columnsFrom": ["workspace_id"], 615 + "columnsTo": ["id"], 616 + "onDelete": "cascade", 617 + "onUpdate": "cascade" 618 + }, 619 + "asset_project_id_project_id_fk": { 620 + "name": "asset_project_id_project_id_fk", 621 + "tableFrom": "asset", 622 + "tableTo": "project", 623 + "columnsFrom": ["project_id"], 624 + "columnsTo": ["id"], 625 + "onDelete": "cascade", 626 + "onUpdate": "cascade" 627 + }, 628 + "asset_task_id_task_id_fk": { 629 + "name": "asset_task_id_task_id_fk", 630 + "tableFrom": "asset", 631 + "tableTo": "task", 632 + "columnsFrom": ["task_id"], 633 + "columnsTo": ["id"], 634 + "onDelete": "cascade", 635 + "onUpdate": "cascade" 636 + }, 637 + "asset_activity_id_activity_id_fk": { 638 + "name": "asset_activity_id_activity_id_fk", 639 + "tableFrom": "asset", 640 + "tableTo": "activity", 641 + "columnsFrom": ["activity_id"], 642 + "columnsTo": ["id"], 643 + "onDelete": "cascade", 644 + "onUpdate": "cascade" 645 + }, 646 + "asset_created_by_user_id_fk": { 647 + "name": "asset_created_by_user_id_fk", 648 + "tableFrom": "asset", 649 + "tableTo": "user", 650 + "columnsFrom": ["created_by"], 651 + "columnsTo": ["id"], 652 + "onDelete": "set null", 653 + "onUpdate": "cascade" 654 + } 655 + }, 656 + "compositePrimaryKeys": {}, 657 + "uniqueConstraints": { 658 + "asset_object_key_unique": { 659 + "name": "asset_object_key_unique", 660 + "nullsNotDistinct": false, 661 + "columns": ["object_key"] 662 + } 663 + }, 664 + "policies": {}, 665 + "checkConstraints": {}, 666 + "isRLSEnabled": false 667 + }, 668 + "public.column": { 669 + "name": "column", 670 + "schema": "", 671 + "columns": { 672 + "id": { 673 + "name": "id", 674 + "type": "text", 675 + "primaryKey": true, 676 + "notNull": true 677 + }, 678 + "project_id": { 679 + "name": "project_id", 680 + "type": "text", 681 + "primaryKey": false, 682 + "notNull": true 683 + }, 684 + "name": { 685 + "name": "name", 686 + "type": "text", 687 + "primaryKey": false, 688 + "notNull": true 689 + }, 690 + "slug": { 691 + "name": "slug", 692 + "type": "text", 693 + "primaryKey": false, 694 + "notNull": true 695 + }, 696 + "position": { 697 + "name": "position", 698 + "type": "integer", 699 + "primaryKey": false, 700 + "notNull": true, 701 + "default": 0 702 + }, 703 + "icon": { 704 + "name": "icon", 705 + "type": "text", 706 + "primaryKey": false, 707 + "notNull": false 708 + }, 709 + "color": { 710 + "name": "color", 711 + "type": "text", 712 + "primaryKey": false, 713 + "notNull": false 714 + }, 715 + "is_final": { 716 + "name": "is_final", 717 + "type": "boolean", 718 + "primaryKey": false, 719 + "notNull": true, 720 + "default": false 721 + }, 722 + "created_at": { 723 + "name": "created_at", 724 + "type": "timestamp", 725 + "primaryKey": false, 726 + "notNull": true, 727 + "default": "now()" 728 + }, 729 + "updated_at": { 730 + "name": "updated_at", 731 + "type": "timestamp", 732 + "primaryKey": false, 733 + "notNull": true, 734 + "default": "now()" 735 + } 736 + }, 737 + "indexes": { 738 + "column_projectId_idx": { 739 + "name": "column_projectId_idx", 740 + "columns": [ 741 + { 742 + "expression": "project_id", 743 + "isExpression": false, 744 + "asc": true, 745 + "nulls": "last" 746 + } 747 + ], 748 + "isUnique": false, 749 + "concurrently": false, 750 + "method": "btree", 751 + "with": {} 752 + } 753 + }, 754 + "foreignKeys": { 755 + "column_project_id_project_id_fk": { 756 + "name": "column_project_id_project_id_fk", 757 + "tableFrom": "column", 758 + "tableTo": "project", 759 + "columnsFrom": ["project_id"], 760 + "columnsTo": ["id"], 761 + "onDelete": "cascade", 762 + "onUpdate": "cascade" 763 + } 764 + }, 765 + "compositePrimaryKeys": {}, 766 + "uniqueConstraints": {}, 767 + "policies": {}, 768 + "checkConstraints": {}, 769 + "isRLSEnabled": false 770 + }, 771 + "public.comment": { 772 + "name": "comment", 773 + "schema": "", 774 + "columns": { 775 + "id": { 776 + "name": "id", 777 + "type": "text", 778 + "primaryKey": true, 779 + "notNull": true 780 + }, 781 + "task_id": { 782 + "name": "task_id", 783 + "type": "text", 784 + "primaryKey": false, 785 + "notNull": true 786 + }, 787 + "user_id": { 788 + "name": "user_id", 789 + "type": "text", 790 + "primaryKey": false, 791 + "notNull": true 792 + }, 793 + "content": { 794 + "name": "content", 795 + "type": "text", 796 + "primaryKey": false, 797 + "notNull": true 798 + }, 799 + "created_at": { 800 + "name": "created_at", 801 + "type": "timestamp", 802 + "primaryKey": false, 803 + "notNull": true, 804 + "default": "now()" 805 + }, 806 + "updated_at": { 807 + "name": "updated_at", 808 + "type": "timestamp", 809 + "primaryKey": false, 810 + "notNull": true, 811 + "default": "now()" 812 + } 813 + }, 814 + "indexes": { 815 + "comment_task_idx": { 816 + "name": "comment_task_idx", 817 + "columns": [ 818 + { 819 + "expression": "task_id", 820 + "isExpression": false, 821 + "asc": true, 822 + "nulls": "last" 823 + } 824 + ], 825 + "isUnique": false, 826 + "concurrently": false, 827 + "method": "btree", 828 + "with": {} 829 + }, 830 + "comment_user_idx": { 831 + "name": "comment_user_idx", 832 + "columns": [ 833 + { 834 + "expression": "user_id", 835 + "isExpression": false, 836 + "asc": true, 837 + "nulls": "last" 838 + } 839 + ], 840 + "isUnique": false, 841 + "concurrently": false, 842 + "method": "btree", 843 + "with": {} 844 + } 845 + }, 846 + "foreignKeys": { 847 + "comment_task_id_task_id_fk": { 848 + "name": "comment_task_id_task_id_fk", 849 + "tableFrom": "comment", 850 + "tableTo": "task", 851 + "columnsFrom": ["task_id"], 852 + "columnsTo": ["id"], 853 + "onDelete": "cascade", 854 + "onUpdate": "cascade" 855 + }, 856 + "comment_user_id_user_id_fk": { 857 + "name": "comment_user_id_user_id_fk", 858 + "tableFrom": "comment", 859 + "tableTo": "user", 860 + "columnsFrom": ["user_id"], 861 + "columnsTo": ["id"], 862 + "onDelete": "cascade", 863 + "onUpdate": "cascade" 864 + } 865 + }, 866 + "compositePrimaryKeys": {}, 867 + "uniqueConstraints": {}, 868 + "policies": {}, 869 + "checkConstraints": {}, 870 + "isRLSEnabled": false 871 + }, 872 + "public.external_link": { 873 + "name": "external_link", 874 + "schema": "", 875 + "columns": { 876 + "id": { 877 + "name": "id", 878 + "type": "text", 879 + "primaryKey": true, 880 + "notNull": true 881 + }, 882 + "task_id": { 883 + "name": "task_id", 884 + "type": "text", 885 + "primaryKey": false, 886 + "notNull": true 887 + }, 888 + "integration_id": { 889 + "name": "integration_id", 890 + "type": "text", 891 + "primaryKey": false, 892 + "notNull": true 893 + }, 894 + "resource_type": { 895 + "name": "resource_type", 896 + "type": "text", 897 + "primaryKey": false, 898 + "notNull": true 899 + }, 900 + "external_id": { 901 + "name": "external_id", 902 + "type": "text", 903 + "primaryKey": false, 904 + "notNull": true 905 + }, 906 + "url": { 907 + "name": "url", 908 + "type": "text", 909 + "primaryKey": false, 910 + "notNull": true 911 + }, 912 + "title": { 913 + "name": "title", 914 + "type": "text", 915 + "primaryKey": false, 916 + "notNull": false 917 + }, 918 + "metadata": { 919 + "name": "metadata", 920 + "type": "text", 921 + "primaryKey": false, 922 + "notNull": false 923 + }, 924 + "created_at": { 925 + "name": "created_at", 926 + "type": "timestamp", 927 + "primaryKey": false, 928 + "notNull": true, 929 + "default": "now()" 930 + }, 931 + "updated_at": { 932 + "name": "updated_at", 933 + "type": "timestamp", 934 + "primaryKey": false, 935 + "notNull": true, 936 + "default": "now()" 937 + } 938 + }, 939 + "indexes": { 940 + "external_link_taskId_idx": { 941 + "name": "external_link_taskId_idx", 942 + "columns": [ 943 + { 944 + "expression": "task_id", 945 + "isExpression": false, 946 + "asc": true, 947 + "nulls": "last" 948 + } 949 + ], 950 + "isUnique": false, 951 + "concurrently": false, 952 + "method": "btree", 953 + "with": {} 954 + }, 955 + "external_link_integrationId_idx": { 956 + "name": "external_link_integrationId_idx", 957 + "columns": [ 958 + { 959 + "expression": "integration_id", 960 + "isExpression": false, 961 + "asc": true, 962 + "nulls": "last" 963 + } 964 + ], 965 + "isUnique": false, 966 + "concurrently": false, 967 + "method": "btree", 968 + "with": {} 969 + }, 970 + "external_link_externalId_idx": { 971 + "name": "external_link_externalId_idx", 972 + "columns": [ 973 + { 974 + "expression": "external_id", 975 + "isExpression": false, 976 + "asc": true, 977 + "nulls": "last" 978 + } 979 + ], 980 + "isUnique": false, 981 + "concurrently": false, 982 + "method": "btree", 983 + "with": {} 984 + }, 985 + "external_link_resourceType_idx": { 986 + "name": "external_link_resourceType_idx", 987 + "columns": [ 988 + { 989 + "expression": "resource_type", 990 + "isExpression": false, 991 + "asc": true, 992 + "nulls": "last" 993 + } 994 + ], 995 + "isUnique": false, 996 + "concurrently": false, 997 + "method": "btree", 998 + "with": {} 999 + } 1000 + }, 1001 + "foreignKeys": { 1002 + "external_link_task_id_task_id_fk": { 1003 + "name": "external_link_task_id_task_id_fk", 1004 + "tableFrom": "external_link", 1005 + "tableTo": "task", 1006 + "columnsFrom": ["task_id"], 1007 + "columnsTo": ["id"], 1008 + "onDelete": "cascade", 1009 + "onUpdate": "cascade" 1010 + }, 1011 + "external_link_integration_id_integration_id_fk": { 1012 + "name": "external_link_integration_id_integration_id_fk", 1013 + "tableFrom": "external_link", 1014 + "tableTo": "integration", 1015 + "columnsFrom": ["integration_id"], 1016 + "columnsTo": ["id"], 1017 + "onDelete": "cascade", 1018 + "onUpdate": "cascade" 1019 + } 1020 + }, 1021 + "compositePrimaryKeys": {}, 1022 + "uniqueConstraints": {}, 1023 + "policies": {}, 1024 + "checkConstraints": {}, 1025 + "isRLSEnabled": false 1026 + }, 1027 + "public.github_integration": { 1028 + "name": "github_integration", 1029 + "schema": "", 1030 + "columns": { 1031 + "id": { 1032 + "name": "id", 1033 + "type": "text", 1034 + "primaryKey": true, 1035 + "notNull": true 1036 + }, 1037 + "project_id": { 1038 + "name": "project_id", 1039 + "type": "text", 1040 + "primaryKey": false, 1041 + "notNull": true 1042 + }, 1043 + "repository_owner": { 1044 + "name": "repository_owner", 1045 + "type": "text", 1046 + "primaryKey": false, 1047 + "notNull": true 1048 + }, 1049 + "repository_name": { 1050 + "name": "repository_name", 1051 + "type": "text", 1052 + "primaryKey": false, 1053 + "notNull": true 1054 + }, 1055 + "installation_id": { 1056 + "name": "installation_id", 1057 + "type": "integer", 1058 + "primaryKey": false, 1059 + "notNull": false 1060 + }, 1061 + "is_active": { 1062 + "name": "is_active", 1063 + "type": "boolean", 1064 + "primaryKey": false, 1065 + "notNull": false, 1066 + "default": true 1067 + }, 1068 + "created_at": { 1069 + "name": "created_at", 1070 + "type": "timestamp", 1071 + "primaryKey": false, 1072 + "notNull": true, 1073 + "default": "now()" 1074 + }, 1075 + "updated_at": { 1076 + "name": "updated_at", 1077 + "type": "timestamp", 1078 + "primaryKey": false, 1079 + "notNull": true, 1080 + "default": "now()" 1081 + } 1082 + }, 1083 + "indexes": {}, 1084 + "foreignKeys": { 1085 + "github_integration_project_id_project_id_fk": { 1086 + "name": "github_integration_project_id_project_id_fk", 1087 + "tableFrom": "github_integration", 1088 + "tableTo": "project", 1089 + "columnsFrom": ["project_id"], 1090 + "columnsTo": ["id"], 1091 + "onDelete": "cascade", 1092 + "onUpdate": "cascade" 1093 + } 1094 + }, 1095 + "compositePrimaryKeys": {}, 1096 + "uniqueConstraints": { 1097 + "github_integration_project_id_unique": { 1098 + "name": "github_integration_project_id_unique", 1099 + "nullsNotDistinct": false, 1100 + "columns": ["project_id"] 1101 + } 1102 + }, 1103 + "policies": {}, 1104 + "checkConstraints": {}, 1105 + "isRLSEnabled": false 1106 + }, 1107 + "public.integration": { 1108 + "name": "integration", 1109 + "schema": "", 1110 + "columns": { 1111 + "id": { 1112 + "name": "id", 1113 + "type": "text", 1114 + "primaryKey": true, 1115 + "notNull": true 1116 + }, 1117 + "project_id": { 1118 + "name": "project_id", 1119 + "type": "text", 1120 + "primaryKey": false, 1121 + "notNull": true 1122 + }, 1123 + "type": { 1124 + "name": "type", 1125 + "type": "text", 1126 + "primaryKey": false, 1127 + "notNull": true 1128 + }, 1129 + "config": { 1130 + "name": "config", 1131 + "type": "text", 1132 + "primaryKey": false, 1133 + "notNull": true 1134 + }, 1135 + "is_active": { 1136 + "name": "is_active", 1137 + "type": "boolean", 1138 + "primaryKey": false, 1139 + "notNull": false, 1140 + "default": true 1141 + }, 1142 + "created_at": { 1143 + "name": "created_at", 1144 + "type": "timestamp", 1145 + "primaryKey": false, 1146 + "notNull": true, 1147 + "default": "now()" 1148 + }, 1149 + "updated_at": { 1150 + "name": "updated_at", 1151 + "type": "timestamp", 1152 + "primaryKey": false, 1153 + "notNull": true, 1154 + "default": "now()" 1155 + } 1156 + }, 1157 + "indexes": { 1158 + "integration_projectId_idx": { 1159 + "name": "integration_projectId_idx", 1160 + "columns": [ 1161 + { 1162 + "expression": "project_id", 1163 + "isExpression": false, 1164 + "asc": true, 1165 + "nulls": "last" 1166 + } 1167 + ], 1168 + "isUnique": false, 1169 + "concurrently": false, 1170 + "method": "btree", 1171 + "with": {} 1172 + }, 1173 + "integration_type_idx": { 1174 + "name": "integration_type_idx", 1175 + "columns": [ 1176 + { 1177 + "expression": "type", 1178 + "isExpression": false, 1179 + "asc": true, 1180 + "nulls": "last" 1181 + } 1182 + ], 1183 + "isUnique": false, 1184 + "concurrently": false, 1185 + "method": "btree", 1186 + "with": {} 1187 + } 1188 + }, 1189 + "foreignKeys": { 1190 + "integration_project_id_project_id_fk": { 1191 + "name": "integration_project_id_project_id_fk", 1192 + "tableFrom": "integration", 1193 + "tableTo": "project", 1194 + "columnsFrom": ["project_id"], 1195 + "columnsTo": ["id"], 1196 + "onDelete": "cascade", 1197 + "onUpdate": "cascade" 1198 + } 1199 + }, 1200 + "compositePrimaryKeys": {}, 1201 + "uniqueConstraints": { 1202 + "integration_project_type_unique": { 1203 + "name": "integration_project_type_unique", 1204 + "nullsNotDistinct": false, 1205 + "columns": ["project_id", "type"] 1206 + } 1207 + }, 1208 + "policies": {}, 1209 + "checkConstraints": {}, 1210 + "isRLSEnabled": false 1211 + }, 1212 + "public.invitation": { 1213 + "name": "invitation", 1214 + "schema": "", 1215 + "columns": { 1216 + "id": { 1217 + "name": "id", 1218 + "type": "text", 1219 + "primaryKey": true, 1220 + "notNull": true 1221 + }, 1222 + "workspace_id": { 1223 + "name": "workspace_id", 1224 + "type": "text", 1225 + "primaryKey": false, 1226 + "notNull": true 1227 + }, 1228 + "email": { 1229 + "name": "email", 1230 + "type": "text", 1231 + "primaryKey": false, 1232 + "notNull": true 1233 + }, 1234 + "role": { 1235 + "name": "role", 1236 + "type": "text", 1237 + "primaryKey": false, 1238 + "notNull": false 1239 + }, 1240 + "team_id": { 1241 + "name": "team_id", 1242 + "type": "text", 1243 + "primaryKey": false, 1244 + "notNull": false 1245 + }, 1246 + "status": { 1247 + "name": "status", 1248 + "type": "text", 1249 + "primaryKey": false, 1250 + "notNull": true, 1251 + "default": "'pending'" 1252 + }, 1253 + "expires_at": { 1254 + "name": "expires_at", 1255 + "type": "timestamp", 1256 + "primaryKey": false, 1257 + "notNull": true 1258 + }, 1259 + "created_at": { 1260 + "name": "created_at", 1261 + "type": "timestamp", 1262 + "primaryKey": false, 1263 + "notNull": true, 1264 + "default": "now()" 1265 + }, 1266 + "inviter_id": { 1267 + "name": "inviter_id", 1268 + "type": "text", 1269 + "primaryKey": false, 1270 + "notNull": true 1271 + } 1272 + }, 1273 + "indexes": { 1274 + "invitation_workspaceId_idx": { 1275 + "name": "invitation_workspaceId_idx", 1276 + "columns": [ 1277 + { 1278 + "expression": "workspace_id", 1279 + "isExpression": false, 1280 + "asc": true, 1281 + "nulls": "last" 1282 + } 1283 + ], 1284 + "isUnique": false, 1285 + "concurrently": false, 1286 + "method": "btree", 1287 + "with": {} 1288 + }, 1289 + "invitation_email_idx": { 1290 + "name": "invitation_email_idx", 1291 + "columns": [ 1292 + { 1293 + "expression": "email", 1294 + "isExpression": false, 1295 + "asc": true, 1296 + "nulls": "last" 1297 + } 1298 + ], 1299 + "isUnique": false, 1300 + "concurrently": false, 1301 + "method": "btree", 1302 + "with": {} 1303 + } 1304 + }, 1305 + "foreignKeys": { 1306 + "invitation_workspace_id_workspace_id_fk": { 1307 + "name": "invitation_workspace_id_workspace_id_fk", 1308 + "tableFrom": "invitation", 1309 + "tableTo": "workspace", 1310 + "columnsFrom": ["workspace_id"], 1311 + "columnsTo": ["id"], 1312 + "onDelete": "cascade", 1313 + "onUpdate": "no action" 1314 + }, 1315 + "invitation_inviter_id_user_id_fk": { 1316 + "name": "invitation_inviter_id_user_id_fk", 1317 + "tableFrom": "invitation", 1318 + "tableTo": "user", 1319 + "columnsFrom": ["inviter_id"], 1320 + "columnsTo": ["id"], 1321 + "onDelete": "cascade", 1322 + "onUpdate": "no action" 1323 + } 1324 + }, 1325 + "compositePrimaryKeys": {}, 1326 + "uniqueConstraints": {}, 1327 + "policies": {}, 1328 + "checkConstraints": {}, 1329 + "isRLSEnabled": false 1330 + }, 1331 + "public.label": { 1332 + "name": "label", 1333 + "schema": "", 1334 + "columns": { 1335 + "id": { 1336 + "name": "id", 1337 + "type": "text", 1338 + "primaryKey": true, 1339 + "notNull": true 1340 + }, 1341 + "name": { 1342 + "name": "name", 1343 + "type": "text", 1344 + "primaryKey": false, 1345 + "notNull": true 1346 + }, 1347 + "color": { 1348 + "name": "color", 1349 + "type": "text", 1350 + "primaryKey": false, 1351 + "notNull": true 1352 + }, 1353 + "created_at": { 1354 + "name": "created_at", 1355 + "type": "timestamp", 1356 + "primaryKey": false, 1357 + "notNull": true, 1358 + "default": "now()" 1359 + }, 1360 + "task_id": { 1361 + "name": "task_id", 1362 + "type": "text", 1363 + "primaryKey": false, 1364 + "notNull": false 1365 + }, 1366 + "workspace_id": { 1367 + "name": "workspace_id", 1368 + "type": "text", 1369 + "primaryKey": false, 1370 + "notNull": false 1371 + } 1372 + }, 1373 + "indexes": {}, 1374 + "foreignKeys": { 1375 + "label_task_id_task_id_fk": { 1376 + "name": "label_task_id_task_id_fk", 1377 + "tableFrom": "label", 1378 + "tableTo": "task", 1379 + "columnsFrom": ["task_id"], 1380 + "columnsTo": ["id"], 1381 + "onDelete": "cascade", 1382 + "onUpdate": "cascade" 1383 + }, 1384 + "label_workspace_id_workspace_id_fk": { 1385 + "name": "label_workspace_id_workspace_id_fk", 1386 + "tableFrom": "label", 1387 + "tableTo": "workspace", 1388 + "columnsFrom": ["workspace_id"], 1389 + "columnsTo": ["id"], 1390 + "onDelete": "cascade", 1391 + "onUpdate": "cascade" 1392 + } 1393 + }, 1394 + "compositePrimaryKeys": {}, 1395 + "uniqueConstraints": {}, 1396 + "policies": {}, 1397 + "checkConstraints": {}, 1398 + "isRLSEnabled": false 1399 + }, 1400 + "public.notification": { 1401 + "name": "notification", 1402 + "schema": "", 1403 + "columns": { 1404 + "id": { 1405 + "name": "id", 1406 + "type": "text", 1407 + "primaryKey": true, 1408 + "notNull": true 1409 + }, 1410 + "user_id": { 1411 + "name": "user_id", 1412 + "type": "text", 1413 + "primaryKey": false, 1414 + "notNull": true 1415 + }, 1416 + "title": { 1417 + "name": "title", 1418 + "type": "text", 1419 + "primaryKey": false, 1420 + "notNull": false 1421 + }, 1422 + "content": { 1423 + "name": "content", 1424 + "type": "text", 1425 + "primaryKey": false, 1426 + "notNull": false 1427 + }, 1428 + "type": { 1429 + "name": "type", 1430 + "type": "text", 1431 + "primaryKey": false, 1432 + "notNull": true, 1433 + "default": "'info'" 1434 + }, 1435 + "event_data": { 1436 + "name": "event_data", 1437 + "type": "jsonb", 1438 + "primaryKey": false, 1439 + "notNull": false 1440 + }, 1441 + "is_read": { 1442 + "name": "is_read", 1443 + "type": "boolean", 1444 + "primaryKey": false, 1445 + "notNull": false, 1446 + "default": false 1447 + }, 1448 + "resource_id": { 1449 + "name": "resource_id", 1450 + "type": "text", 1451 + "primaryKey": false, 1452 + "notNull": false 1453 + }, 1454 + "resource_type": { 1455 + "name": "resource_type", 1456 + "type": "text", 1457 + "primaryKey": false, 1458 + "notNull": false 1459 + }, 1460 + "created_at": { 1461 + "name": "created_at", 1462 + "type": "timestamp with time zone", 1463 + "primaryKey": false, 1464 + "notNull": true, 1465 + "default": "now()" 1466 + } 1467 + }, 1468 + "indexes": {}, 1469 + "foreignKeys": { 1470 + "notification_user_id_user_id_fk": { 1471 + "name": "notification_user_id_user_id_fk", 1472 + "tableFrom": "notification", 1473 + "tableTo": "user", 1474 + "columnsFrom": ["user_id"], 1475 + "columnsTo": ["id"], 1476 + "onDelete": "cascade", 1477 + "onUpdate": "cascade" 1478 + } 1479 + }, 1480 + "compositePrimaryKeys": {}, 1481 + "uniqueConstraints": {}, 1482 + "policies": {}, 1483 + "checkConstraints": {}, 1484 + "isRLSEnabled": false 1485 + }, 1486 + "public.project": { 1487 + "name": "project", 1488 + "schema": "", 1489 + "columns": { 1490 + "id": { 1491 + "name": "id", 1492 + "type": "text", 1493 + "primaryKey": true, 1494 + "notNull": true 1495 + }, 1496 + "workspace_id": { 1497 + "name": "workspace_id", 1498 + "type": "text", 1499 + "primaryKey": false, 1500 + "notNull": true 1501 + }, 1502 + "slug": { 1503 + "name": "slug", 1504 + "type": "text", 1505 + "primaryKey": false, 1506 + "notNull": true 1507 + }, 1508 + "icon": { 1509 + "name": "icon", 1510 + "type": "text", 1511 + "primaryKey": false, 1512 + "notNull": false, 1513 + "default": "'Layout'" 1514 + }, 1515 + "name": { 1516 + "name": "name", 1517 + "type": "text", 1518 + "primaryKey": false, 1519 + "notNull": true 1520 + }, 1521 + "description": { 1522 + "name": "description", 1523 + "type": "text", 1524 + "primaryKey": false, 1525 + "notNull": false 1526 + }, 1527 + "created_at": { 1528 + "name": "created_at", 1529 + "type": "timestamp", 1530 + "primaryKey": false, 1531 + "notNull": true, 1532 + "default": "now()" 1533 + }, 1534 + "is_public": { 1535 + "name": "is_public", 1536 + "type": "boolean", 1537 + "primaryKey": false, 1538 + "notNull": false, 1539 + "default": false 1540 + }, 1541 + "archived_at": { 1542 + "name": "archived_at", 1543 + "type": "timestamp", 1544 + "primaryKey": false, 1545 + "notNull": false 1546 + } 1547 + }, 1548 + "indexes": {}, 1549 + "foreignKeys": { 1550 + "project_workspace_id_workspace_id_fk": { 1551 + "name": "project_workspace_id_workspace_id_fk", 1552 + "tableFrom": "project", 1553 + "tableTo": "workspace", 1554 + "columnsFrom": ["workspace_id"], 1555 + "columnsTo": ["id"], 1556 + "onDelete": "cascade", 1557 + "onUpdate": "cascade" 1558 + } 1559 + }, 1560 + "compositePrimaryKeys": {}, 1561 + "uniqueConstraints": {}, 1562 + "policies": {}, 1563 + "checkConstraints": {}, 1564 + "isRLSEnabled": false 1565 + }, 1566 + "public.session": { 1567 + "name": "session", 1568 + "schema": "", 1569 + "columns": { 1570 + "id": { 1571 + "name": "id", 1572 + "type": "text", 1573 + "primaryKey": true, 1574 + "notNull": true 1575 + }, 1576 + "expires_at": { 1577 + "name": "expires_at", 1578 + "type": "timestamp", 1579 + "primaryKey": false, 1580 + "notNull": true 1581 + }, 1582 + "token": { 1583 + "name": "token", 1584 + "type": "text", 1585 + "primaryKey": false, 1586 + "notNull": true 1587 + }, 1588 + "created_at": { 1589 + "name": "created_at", 1590 + "type": "timestamp", 1591 + "primaryKey": false, 1592 + "notNull": true, 1593 + "default": "now()" 1594 + }, 1595 + "updated_at": { 1596 + "name": "updated_at", 1597 + "type": "timestamp", 1598 + "primaryKey": false, 1599 + "notNull": true 1600 + }, 1601 + "ip_address": { 1602 + "name": "ip_address", 1603 + "type": "text", 1604 + "primaryKey": false, 1605 + "notNull": false 1606 + }, 1607 + "user_agent": { 1608 + "name": "user_agent", 1609 + "type": "text", 1610 + "primaryKey": false, 1611 + "notNull": false 1612 + }, 1613 + "user_id": { 1614 + "name": "user_id", 1615 + "type": "text", 1616 + "primaryKey": false, 1617 + "notNull": true 1618 + }, 1619 + "active_organization_id": { 1620 + "name": "active_organization_id", 1621 + "type": "text", 1622 + "primaryKey": false, 1623 + "notNull": false 1624 + }, 1625 + "active_team_id": { 1626 + "name": "active_team_id", 1627 + "type": "text", 1628 + "primaryKey": false, 1629 + "notNull": false 1630 + } 1631 + }, 1632 + "indexes": { 1633 + "session_userId_idx": { 1634 + "name": "session_userId_idx", 1635 + "columns": [ 1636 + { 1637 + "expression": "user_id", 1638 + "isExpression": false, 1639 + "asc": true, 1640 + "nulls": "last" 1641 + } 1642 + ], 1643 + "isUnique": false, 1644 + "concurrently": false, 1645 + "method": "btree", 1646 + "with": {} 1647 + } 1648 + }, 1649 + "foreignKeys": { 1650 + "session_user_id_user_id_fk": { 1651 + "name": "session_user_id_user_id_fk", 1652 + "tableFrom": "session", 1653 + "tableTo": "user", 1654 + "columnsFrom": ["user_id"], 1655 + "columnsTo": ["id"], 1656 + "onDelete": "cascade", 1657 + "onUpdate": "no action" 1658 + } 1659 + }, 1660 + "compositePrimaryKeys": {}, 1661 + "uniqueConstraints": { 1662 + "session_token_unique": { 1663 + "name": "session_token_unique", 1664 + "nullsNotDistinct": false, 1665 + "columns": ["token"] 1666 + } 1667 + }, 1668 + "policies": {}, 1669 + "checkConstraints": {}, 1670 + "isRLSEnabled": false 1671 + }, 1672 + "public.task_relation": { 1673 + "name": "task_relation", 1674 + "schema": "", 1675 + "columns": { 1676 + "id": { 1677 + "name": "id", 1678 + "type": "text", 1679 + "primaryKey": true, 1680 + "notNull": true 1681 + }, 1682 + "source_task_id": { 1683 + "name": "source_task_id", 1684 + "type": "text", 1685 + "primaryKey": false, 1686 + "notNull": true 1687 + }, 1688 + "target_task_id": { 1689 + "name": "target_task_id", 1690 + "type": "text", 1691 + "primaryKey": false, 1692 + "notNull": true 1693 + }, 1694 + "relation_type": { 1695 + "name": "relation_type", 1696 + "type": "text", 1697 + "primaryKey": false, 1698 + "notNull": true 1699 + }, 1700 + "created_at": { 1701 + "name": "created_at", 1702 + "type": "timestamp", 1703 + "primaryKey": false, 1704 + "notNull": true, 1705 + "default": "now()" 1706 + } 1707 + }, 1708 + "indexes": { 1709 + "task_relation_source_idx": { 1710 + "name": "task_relation_source_idx", 1711 + "columns": [ 1712 + { 1713 + "expression": "source_task_id", 1714 + "isExpression": false, 1715 + "asc": true, 1716 + "nulls": "last" 1717 + } 1718 + ], 1719 + "isUnique": false, 1720 + "concurrently": false, 1721 + "method": "btree", 1722 + "with": {} 1723 + }, 1724 + "task_relation_target_idx": { 1725 + "name": "task_relation_target_idx", 1726 + "columns": [ 1727 + { 1728 + "expression": "target_task_id", 1729 + "isExpression": false, 1730 + "asc": true, 1731 + "nulls": "last" 1732 + } 1733 + ], 1734 + "isUnique": false, 1735 + "concurrently": false, 1736 + "method": "btree", 1737 + "with": {} 1738 + } 1739 + }, 1740 + "foreignKeys": { 1741 + "task_relation_source_task_id_task_id_fk": { 1742 + "name": "task_relation_source_task_id_task_id_fk", 1743 + "tableFrom": "task_relation", 1744 + "tableTo": "task", 1745 + "columnsFrom": ["source_task_id"], 1746 + "columnsTo": ["id"], 1747 + "onDelete": "cascade", 1748 + "onUpdate": "cascade" 1749 + }, 1750 + "task_relation_target_task_id_task_id_fk": { 1751 + "name": "task_relation_target_task_id_task_id_fk", 1752 + "tableFrom": "task_relation", 1753 + "tableTo": "task", 1754 + "columnsFrom": ["target_task_id"], 1755 + "columnsTo": ["id"], 1756 + "onDelete": "cascade", 1757 + "onUpdate": "cascade" 1758 + } 1759 + }, 1760 + "compositePrimaryKeys": {}, 1761 + "uniqueConstraints": {}, 1762 + "policies": {}, 1763 + "checkConstraints": {}, 1764 + "isRLSEnabled": false 1765 + }, 1766 + "public.task": { 1767 + "name": "task", 1768 + "schema": "", 1769 + "columns": { 1770 + "id": { 1771 + "name": "id", 1772 + "type": "text", 1773 + "primaryKey": true, 1774 + "notNull": true 1775 + }, 1776 + "project_id": { 1777 + "name": "project_id", 1778 + "type": "text", 1779 + "primaryKey": false, 1780 + "notNull": true 1781 + }, 1782 + "position": { 1783 + "name": "position", 1784 + "type": "integer", 1785 + "primaryKey": false, 1786 + "notNull": false, 1787 + "default": 0 1788 + }, 1789 + "number": { 1790 + "name": "number", 1791 + "type": "integer", 1792 + "primaryKey": false, 1793 + "notNull": false, 1794 + "default": 1 1795 + }, 1796 + "assignee_id": { 1797 + "name": "assignee_id", 1798 + "type": "text", 1799 + "primaryKey": false, 1800 + "notNull": false 1801 + }, 1802 + "title": { 1803 + "name": "title", 1804 + "type": "text", 1805 + "primaryKey": false, 1806 + "notNull": true 1807 + }, 1808 + "description": { 1809 + "name": "description", 1810 + "type": "text", 1811 + "primaryKey": false, 1812 + "notNull": false 1813 + }, 1814 + "status": { 1815 + "name": "status", 1816 + "type": "text", 1817 + "primaryKey": false, 1818 + "notNull": true, 1819 + "default": "'to-do'" 1820 + }, 1821 + "column_id": { 1822 + "name": "column_id", 1823 + "type": "text", 1824 + "primaryKey": false, 1825 + "notNull": false 1826 + }, 1827 + "priority": { 1828 + "name": "priority", 1829 + "type": "text", 1830 + "primaryKey": false, 1831 + "notNull": false, 1832 + "default": "'low'" 1833 + }, 1834 + "start_date": { 1835 + "name": "start_date", 1836 + "type": "timestamp", 1837 + "primaryKey": false, 1838 + "notNull": false 1839 + }, 1840 + "due_date": { 1841 + "name": "due_date", 1842 + "type": "timestamp", 1843 + "primaryKey": false, 1844 + "notNull": false 1845 + }, 1846 + "created_at": { 1847 + "name": "created_at", 1848 + "type": "timestamp", 1849 + "primaryKey": false, 1850 + "notNull": true, 1851 + "default": "now()" 1852 + } 1853 + }, 1854 + "indexes": {}, 1855 + "foreignKeys": { 1856 + "task_project_id_project_id_fk": { 1857 + "name": "task_project_id_project_id_fk", 1858 + "tableFrom": "task", 1859 + "tableTo": "project", 1860 + "columnsFrom": ["project_id"], 1861 + "columnsTo": ["id"], 1862 + "onDelete": "cascade", 1863 + "onUpdate": "cascade" 1864 + }, 1865 + "task_assignee_id_user_id_fk": { 1866 + "name": "task_assignee_id_user_id_fk", 1867 + "tableFrom": "task", 1868 + "tableTo": "user", 1869 + "columnsFrom": ["assignee_id"], 1870 + "columnsTo": ["id"], 1871 + "onDelete": "cascade", 1872 + "onUpdate": "cascade" 1873 + }, 1874 + "task_column_id_column_id_fk": { 1875 + "name": "task_column_id_column_id_fk", 1876 + "tableFrom": "task", 1877 + "tableTo": "column", 1878 + "columnsFrom": ["column_id"], 1879 + "columnsTo": ["id"], 1880 + "onDelete": "set null", 1881 + "onUpdate": "cascade" 1882 + } 1883 + }, 1884 + "compositePrimaryKeys": {}, 1885 + "uniqueConstraints": {}, 1886 + "policies": {}, 1887 + "checkConstraints": {}, 1888 + "isRLSEnabled": false 1889 + }, 1890 + "public.team": { 1891 + "name": "team", 1892 + "schema": "", 1893 + "columns": { 1894 + "id": { 1895 + "name": "id", 1896 + "type": "text", 1897 + "primaryKey": true, 1898 + "notNull": true 1899 + }, 1900 + "name": { 1901 + "name": "name", 1902 + "type": "text", 1903 + "primaryKey": false, 1904 + "notNull": true 1905 + }, 1906 + "workspace_id": { 1907 + "name": "workspace_id", 1908 + "type": "text", 1909 + "primaryKey": false, 1910 + "notNull": true 1911 + }, 1912 + "created_at": { 1913 + "name": "created_at", 1914 + "type": "timestamp", 1915 + "primaryKey": false, 1916 + "notNull": true 1917 + }, 1918 + "updated_at": { 1919 + "name": "updated_at", 1920 + "type": "timestamp", 1921 + "primaryKey": false, 1922 + "notNull": false 1923 + } 1924 + }, 1925 + "indexes": { 1926 + "team_workspaceId_idx": { 1927 + "name": "team_workspaceId_idx", 1928 + "columns": [ 1929 + { 1930 + "expression": "workspace_id", 1931 + "isExpression": false, 1932 + "asc": true, 1933 + "nulls": "last" 1934 + } 1935 + ], 1936 + "isUnique": false, 1937 + "concurrently": false, 1938 + "method": "btree", 1939 + "with": {} 1940 + } 1941 + }, 1942 + "foreignKeys": { 1943 + "team_workspace_id_workspace_id_fk": { 1944 + "name": "team_workspace_id_workspace_id_fk", 1945 + "tableFrom": "team", 1946 + "tableTo": "workspace", 1947 + "columnsFrom": ["workspace_id"], 1948 + "columnsTo": ["id"], 1949 + "onDelete": "cascade", 1950 + "onUpdate": "no action" 1951 + } 1952 + }, 1953 + "compositePrimaryKeys": {}, 1954 + "uniqueConstraints": {}, 1955 + "policies": {}, 1956 + "checkConstraints": {}, 1957 + "isRLSEnabled": false 1958 + }, 1959 + "public.team_member": { 1960 + "name": "team_member", 1961 + "schema": "", 1962 + "columns": { 1963 + "id": { 1964 + "name": "id", 1965 + "type": "text", 1966 + "primaryKey": true, 1967 + "notNull": true 1968 + }, 1969 + "team_id": { 1970 + "name": "team_id", 1971 + "type": "text", 1972 + "primaryKey": false, 1973 + "notNull": true 1974 + }, 1975 + "user_id": { 1976 + "name": "user_id", 1977 + "type": "text", 1978 + "primaryKey": false, 1979 + "notNull": true 1980 + }, 1981 + "created_at": { 1982 + "name": "created_at", 1983 + "type": "timestamp", 1984 + "primaryKey": false, 1985 + "notNull": false 1986 + } 1987 + }, 1988 + "indexes": { 1989 + "teamMember_teamId_idx": { 1990 + "name": "teamMember_teamId_idx", 1991 + "columns": [ 1992 + { 1993 + "expression": "team_id", 1994 + "isExpression": false, 1995 + "asc": true, 1996 + "nulls": "last" 1997 + } 1998 + ], 1999 + "isUnique": false, 2000 + "concurrently": false, 2001 + "method": "btree", 2002 + "with": {} 2003 + }, 2004 + "teamMember_userId_idx": { 2005 + "name": "teamMember_userId_idx", 2006 + "columns": [ 2007 + { 2008 + "expression": "user_id", 2009 + "isExpression": false, 2010 + "asc": true, 2011 + "nulls": "last" 2012 + } 2013 + ], 2014 + "isUnique": false, 2015 + "concurrently": false, 2016 + "method": "btree", 2017 + "with": {} 2018 + } 2019 + }, 2020 + "foreignKeys": { 2021 + "team_member_team_id_team_id_fk": { 2022 + "name": "team_member_team_id_team_id_fk", 2023 + "tableFrom": "team_member", 2024 + "tableTo": "team", 2025 + "columnsFrom": ["team_id"], 2026 + "columnsTo": ["id"], 2027 + "onDelete": "cascade", 2028 + "onUpdate": "no action" 2029 + }, 2030 + "team_member_user_id_user_id_fk": { 2031 + "name": "team_member_user_id_user_id_fk", 2032 + "tableFrom": "team_member", 2033 + "tableTo": "user", 2034 + "columnsFrom": ["user_id"], 2035 + "columnsTo": ["id"], 2036 + "onDelete": "cascade", 2037 + "onUpdate": "no action" 2038 + } 2039 + }, 2040 + "compositePrimaryKeys": {}, 2041 + "uniqueConstraints": {}, 2042 + "policies": {}, 2043 + "checkConstraints": {}, 2044 + "isRLSEnabled": false 2045 + }, 2046 + "public.time_entry": { 2047 + "name": "time_entry", 2048 + "schema": "", 2049 + "columns": { 2050 + "id": { 2051 + "name": "id", 2052 + "type": "text", 2053 + "primaryKey": true, 2054 + "notNull": true 2055 + }, 2056 + "task_id": { 2057 + "name": "task_id", 2058 + "type": "text", 2059 + "primaryKey": false, 2060 + "notNull": true 2061 + }, 2062 + "user_id": { 2063 + "name": "user_id", 2064 + "type": "text", 2065 + "primaryKey": false, 2066 + "notNull": false 2067 + }, 2068 + "description": { 2069 + "name": "description", 2070 + "type": "text", 2071 + "primaryKey": false, 2072 + "notNull": false 2073 + }, 2074 + "start_time": { 2075 + "name": "start_time", 2076 + "type": "timestamp", 2077 + "primaryKey": false, 2078 + "notNull": true 2079 + }, 2080 + "end_time": { 2081 + "name": "end_time", 2082 + "type": "timestamp", 2083 + "primaryKey": false, 2084 + "notNull": false 2085 + }, 2086 + "duration": { 2087 + "name": "duration", 2088 + "type": "integer", 2089 + "primaryKey": false, 2090 + "notNull": false, 2091 + "default": 0 2092 + }, 2093 + "created_at": { 2094 + "name": "created_at", 2095 + "type": "timestamp", 2096 + "primaryKey": false, 2097 + "notNull": true, 2098 + "default": "now()" 2099 + } 2100 + }, 2101 + "indexes": {}, 2102 + "foreignKeys": { 2103 + "time_entry_task_id_task_id_fk": { 2104 + "name": "time_entry_task_id_task_id_fk", 2105 + "tableFrom": "time_entry", 2106 + "tableTo": "task", 2107 + "columnsFrom": ["task_id"], 2108 + "columnsTo": ["id"], 2109 + "onDelete": "cascade", 2110 + "onUpdate": "cascade" 2111 + }, 2112 + "time_entry_user_id_user_id_fk": { 2113 + "name": "time_entry_user_id_user_id_fk", 2114 + "tableFrom": "time_entry", 2115 + "tableTo": "user", 2116 + "columnsFrom": ["user_id"], 2117 + "columnsTo": ["id"], 2118 + "onDelete": "cascade", 2119 + "onUpdate": "cascade" 2120 + } 2121 + }, 2122 + "compositePrimaryKeys": {}, 2123 + "uniqueConstraints": {}, 2124 + "policies": {}, 2125 + "checkConstraints": {}, 2126 + "isRLSEnabled": false 2127 + }, 2128 + "public.user": { 2129 + "name": "user", 2130 + "schema": "", 2131 + "columns": { 2132 + "id": { 2133 + "name": "id", 2134 + "type": "text", 2135 + "primaryKey": true, 2136 + "notNull": true 2137 + }, 2138 + "name": { 2139 + "name": "name", 2140 + "type": "text", 2141 + "primaryKey": false, 2142 + "notNull": true 2143 + }, 2144 + "email": { 2145 + "name": "email", 2146 + "type": "text", 2147 + "primaryKey": false, 2148 + "notNull": true 2149 + }, 2150 + "email_verified": { 2151 + "name": "email_verified", 2152 + "type": "boolean", 2153 + "primaryKey": false, 2154 + "notNull": true 2155 + }, 2156 + "image": { 2157 + "name": "image", 2158 + "type": "text", 2159 + "primaryKey": false, 2160 + "notNull": false 2161 + }, 2162 + "locale": { 2163 + "name": "locale", 2164 + "type": "text", 2165 + "primaryKey": false, 2166 + "notNull": false 2167 + }, 2168 + "created_at": { 2169 + "name": "created_at", 2170 + "type": "timestamp", 2171 + "primaryKey": false, 2172 + "notNull": true, 2173 + "default": "now()" 2174 + }, 2175 + "updated_at": { 2176 + "name": "updated_at", 2177 + "type": "timestamp", 2178 + "primaryKey": false, 2179 + "notNull": true, 2180 + "default": "now()" 2181 + }, 2182 + "is_anonymous": { 2183 + "name": "is_anonymous", 2184 + "type": "boolean", 2185 + "primaryKey": false, 2186 + "notNull": false, 2187 + "default": false 2188 + } 2189 + }, 2190 + "indexes": {}, 2191 + "foreignKeys": {}, 2192 + "compositePrimaryKeys": {}, 2193 + "uniqueConstraints": { 2194 + "user_email_unique": { 2195 + "name": "user_email_unique", 2196 + "nullsNotDistinct": false, 2197 + "columns": ["email"] 2198 + } 2199 + }, 2200 + "policies": {}, 2201 + "checkConstraints": {}, 2202 + "isRLSEnabled": false 2203 + }, 2204 + "public.user_notification_preference": { 2205 + "name": "user_notification_preference", 2206 + "schema": "", 2207 + "columns": { 2208 + "user_id": { 2209 + "name": "user_id", 2210 + "type": "text", 2211 + "primaryKey": true, 2212 + "notNull": true 2213 + }, 2214 + "email_enabled": { 2215 + "name": "email_enabled", 2216 + "type": "boolean", 2217 + "primaryKey": false, 2218 + "notNull": true, 2219 + "default": false 2220 + }, 2221 + "ntfy_enabled": { 2222 + "name": "ntfy_enabled", 2223 + "type": "boolean", 2224 + "primaryKey": false, 2225 + "notNull": true, 2226 + "default": false 2227 + }, 2228 + "ntfy_server_url": { 2229 + "name": "ntfy_server_url", 2230 + "type": "text", 2231 + "primaryKey": false, 2232 + "notNull": false 2233 + }, 2234 + "ntfy_topic": { 2235 + "name": "ntfy_topic", 2236 + "type": "text", 2237 + "primaryKey": false, 2238 + "notNull": false 2239 + }, 2240 + "ntfy_token": { 2241 + "name": "ntfy_token", 2242 + "type": "text", 2243 + "primaryKey": false, 2244 + "notNull": false 2245 + }, 2246 + "gotify_enabled": { 2247 + "name": "gotify_enabled", 2248 + "type": "boolean", 2249 + "primaryKey": false, 2250 + "notNull": true, 2251 + "default": false 2252 + }, 2253 + "gotify_server_url": { 2254 + "name": "gotify_server_url", 2255 + "type": "text", 2256 + "primaryKey": false, 2257 + "notNull": false 2258 + }, 2259 + "gotify_token": { 2260 + "name": "gotify_token", 2261 + "type": "text", 2262 + "primaryKey": false, 2263 + "notNull": false 2264 + }, 2265 + "webhook_enabled": { 2266 + "name": "webhook_enabled", 2267 + "type": "boolean", 2268 + "primaryKey": false, 2269 + "notNull": true, 2270 + "default": false 2271 + }, 2272 + "webhook_url": { 2273 + "name": "webhook_url", 2274 + "type": "text", 2275 + "primaryKey": false, 2276 + "notNull": false 2277 + }, 2278 + "webhook_secret": { 2279 + "name": "webhook_secret", 2280 + "type": "text", 2281 + "primaryKey": false, 2282 + "notNull": false 2283 + }, 2284 + "created_at": { 2285 + "name": "created_at", 2286 + "type": "timestamp", 2287 + "primaryKey": false, 2288 + "notNull": true, 2289 + "default": "now()" 2290 + }, 2291 + "updated_at": { 2292 + "name": "updated_at", 2293 + "type": "timestamp", 2294 + "primaryKey": false, 2295 + "notNull": true, 2296 + "default": "now()" 2297 + } 2298 + }, 2299 + "indexes": {}, 2300 + "foreignKeys": { 2301 + "user_notification_preference_user_id_user_id_fk": { 2302 + "name": "user_notification_preference_user_id_user_id_fk", 2303 + "tableFrom": "user_notification_preference", 2304 + "tableTo": "user", 2305 + "columnsFrom": ["user_id"], 2306 + "columnsTo": ["id"], 2307 + "onDelete": "cascade", 2308 + "onUpdate": "cascade" 2309 + } 2310 + }, 2311 + "compositePrimaryKeys": {}, 2312 + "uniqueConstraints": {}, 2313 + "policies": {}, 2314 + "checkConstraints": {}, 2315 + "isRLSEnabled": false 2316 + }, 2317 + "public.user_notification_workspace_project": { 2318 + "name": "user_notification_workspace_project", 2319 + "schema": "", 2320 + "columns": { 2321 + "id": { 2322 + "name": "id", 2323 + "type": "text", 2324 + "primaryKey": true, 2325 + "notNull": true 2326 + }, 2327 + "workspace_rule_id": { 2328 + "name": "workspace_rule_id", 2329 + "type": "text", 2330 + "primaryKey": false, 2331 + "notNull": true 2332 + }, 2333 + "project_id": { 2334 + "name": "project_id", 2335 + "type": "text", 2336 + "primaryKey": false, 2337 + "notNull": true 2338 + }, 2339 + "created_at": { 2340 + "name": "created_at", 2341 + "type": "timestamp", 2342 + "primaryKey": false, 2343 + "notNull": true, 2344 + "default": "now()" 2345 + } 2346 + }, 2347 + "indexes": { 2348 + "user_notification_workspace_project_ruleId_idx": { 2349 + "name": "user_notification_workspace_project_ruleId_idx", 2350 + "columns": [ 2351 + { 2352 + "expression": "workspace_rule_id", 2353 + "isExpression": false, 2354 + "asc": true, 2355 + "nulls": "last" 2356 + } 2357 + ], 2358 + "isUnique": false, 2359 + "concurrently": false, 2360 + "method": "btree", 2361 + "with": {} 2362 + }, 2363 + "user_notification_workspace_project_projectId_idx": { 2364 + "name": "user_notification_workspace_project_projectId_idx", 2365 + "columns": [ 2366 + { 2367 + "expression": "project_id", 2368 + "isExpression": false, 2369 + "asc": true, 2370 + "nulls": "last" 2371 + } 2372 + ], 2373 + "isUnique": false, 2374 + "concurrently": false, 2375 + "method": "btree", 2376 + "with": {} 2377 + } 2378 + }, 2379 + "foreignKeys": { 2380 + "user_notification_workspace_project_workspace_rule_id_user_notification_workspace_rule_id_fk": { 2381 + "name": "user_notification_workspace_project_workspace_rule_id_user_notification_workspace_rule_id_fk", 2382 + "tableFrom": "user_notification_workspace_project", 2383 + "tableTo": "user_notification_workspace_rule", 2384 + "columnsFrom": ["workspace_rule_id"], 2385 + "columnsTo": ["id"], 2386 + "onDelete": "cascade", 2387 + "onUpdate": "cascade" 2388 + }, 2389 + "user_notification_workspace_project_project_id_project_id_fk": { 2390 + "name": "user_notification_workspace_project_project_id_project_id_fk", 2391 + "tableFrom": "user_notification_workspace_project", 2392 + "tableTo": "project", 2393 + "columnsFrom": ["project_id"], 2394 + "columnsTo": ["id"], 2395 + "onDelete": "cascade", 2396 + "onUpdate": "cascade" 2397 + } 2398 + }, 2399 + "compositePrimaryKeys": {}, 2400 + "uniqueConstraints": { 2401 + "user_notification_workspace_project_rule_project_unique": { 2402 + "name": "user_notification_workspace_project_rule_project_unique", 2403 + "nullsNotDistinct": false, 2404 + "columns": ["workspace_rule_id", "project_id"] 2405 + } 2406 + }, 2407 + "policies": {}, 2408 + "checkConstraints": {}, 2409 + "isRLSEnabled": false 2410 + }, 2411 + "public.user_notification_workspace_rule": { 2412 + "name": "user_notification_workspace_rule", 2413 + "schema": "", 2414 + "columns": { 2415 + "id": { 2416 + "name": "id", 2417 + "type": "text", 2418 + "primaryKey": true, 2419 + "notNull": true 2420 + }, 2421 + "user_id": { 2422 + "name": "user_id", 2423 + "type": "text", 2424 + "primaryKey": false, 2425 + "notNull": true 2426 + }, 2427 + "workspace_id": { 2428 + "name": "workspace_id", 2429 + "type": "text", 2430 + "primaryKey": false, 2431 + "notNull": true 2432 + }, 2433 + "is_active": { 2434 + "name": "is_active", 2435 + "type": "boolean", 2436 + "primaryKey": false, 2437 + "notNull": true, 2438 + "default": true 2439 + }, 2440 + "email_enabled": { 2441 + "name": "email_enabled", 2442 + "type": "boolean", 2443 + "primaryKey": false, 2444 + "notNull": true, 2445 + "default": false 2446 + }, 2447 + "ntfy_enabled": { 2448 + "name": "ntfy_enabled", 2449 + "type": "boolean", 2450 + "primaryKey": false, 2451 + "notNull": true, 2452 + "default": false 2453 + }, 2454 + "gotify_enabled": { 2455 + "name": "gotify_enabled", 2456 + "type": "boolean", 2457 + "primaryKey": false, 2458 + "notNull": true, 2459 + "default": false 2460 + }, 2461 + "webhook_enabled": { 2462 + "name": "webhook_enabled", 2463 + "type": "boolean", 2464 + "primaryKey": false, 2465 + "notNull": true, 2466 + "default": false 2467 + }, 2468 + "project_mode": { 2469 + "name": "project_mode", 2470 + "type": "text", 2471 + "primaryKey": false, 2472 + "notNull": true, 2473 + "default": "'all'" 2474 + }, 2475 + "created_at": { 2476 + "name": "created_at", 2477 + "type": "timestamp", 2478 + "primaryKey": false, 2479 + "notNull": true, 2480 + "default": "now()" 2481 + }, 2482 + "updated_at": { 2483 + "name": "updated_at", 2484 + "type": "timestamp", 2485 + "primaryKey": false, 2486 + "notNull": true, 2487 + "default": "now()" 2488 + } 2489 + }, 2490 + "indexes": { 2491 + "user_notification_workspace_rule_userId_idx": { 2492 + "name": "user_notification_workspace_rule_userId_idx", 2493 + "columns": [ 2494 + { 2495 + "expression": "user_id", 2496 + "isExpression": false, 2497 + "asc": true, 2498 + "nulls": "last" 2499 + } 2500 + ], 2501 + "isUnique": false, 2502 + "concurrently": false, 2503 + "method": "btree", 2504 + "with": {} 2505 + }, 2506 + "user_notification_workspace_rule_workspaceId_idx": { 2507 + "name": "user_notification_workspace_rule_workspaceId_idx", 2508 + "columns": [ 2509 + { 2510 + "expression": "workspace_id", 2511 + "isExpression": false, 2512 + "asc": true, 2513 + "nulls": "last" 2514 + } 2515 + ], 2516 + "isUnique": false, 2517 + "concurrently": false, 2518 + "method": "btree", 2519 + "with": {} 2520 + } 2521 + }, 2522 + "foreignKeys": { 2523 + "user_notification_workspace_rule_user_id_user_id_fk": { 2524 + "name": "user_notification_workspace_rule_user_id_user_id_fk", 2525 + "tableFrom": "user_notification_workspace_rule", 2526 + "tableTo": "user", 2527 + "columnsFrom": ["user_id"], 2528 + "columnsTo": ["id"], 2529 + "onDelete": "cascade", 2530 + "onUpdate": "cascade" 2531 + }, 2532 + "user_notification_workspace_rule_workspace_id_workspace_id_fk": { 2533 + "name": "user_notification_workspace_rule_workspace_id_workspace_id_fk", 2534 + "tableFrom": "user_notification_workspace_rule", 2535 + "tableTo": "workspace", 2536 + "columnsFrom": ["workspace_id"], 2537 + "columnsTo": ["id"], 2538 + "onDelete": "cascade", 2539 + "onUpdate": "cascade" 2540 + } 2541 + }, 2542 + "compositePrimaryKeys": {}, 2543 + "uniqueConstraints": { 2544 + "user_notification_workspace_rule_user_workspace_unique": { 2545 + "name": "user_notification_workspace_rule_user_workspace_unique", 2546 + "nullsNotDistinct": false, 2547 + "columns": ["user_id", "workspace_id"] 2548 + } 2549 + }, 2550 + "policies": {}, 2551 + "checkConstraints": {}, 2552 + "isRLSEnabled": false 2553 + }, 2554 + "public.verification": { 2555 + "name": "verification", 2556 + "schema": "", 2557 + "columns": { 2558 + "id": { 2559 + "name": "id", 2560 + "type": "text", 2561 + "primaryKey": true, 2562 + "notNull": true 2563 + }, 2564 + "identifier": { 2565 + "name": "identifier", 2566 + "type": "text", 2567 + "primaryKey": false, 2568 + "notNull": true 2569 + }, 2570 + "value": { 2571 + "name": "value", 2572 + "type": "text", 2573 + "primaryKey": false, 2574 + "notNull": true 2575 + }, 2576 + "expires_at": { 2577 + "name": "expires_at", 2578 + "type": "timestamp", 2579 + "primaryKey": false, 2580 + "notNull": true 2581 + }, 2582 + "created_at": { 2583 + "name": "created_at", 2584 + "type": "timestamp", 2585 + "primaryKey": false, 2586 + "notNull": true, 2587 + "default": "now()" 2588 + }, 2589 + "updated_at": { 2590 + "name": "updated_at", 2591 + "type": "timestamp", 2592 + "primaryKey": false, 2593 + "notNull": true, 2594 + "default": "now()" 2595 + } 2596 + }, 2597 + "indexes": { 2598 + "verification_identifier_idx": { 2599 + "name": "verification_identifier_idx", 2600 + "columns": [ 2601 + { 2602 + "expression": "identifier", 2603 + "isExpression": false, 2604 + "asc": true, 2605 + "nulls": "last" 2606 + } 2607 + ], 2608 + "isUnique": false, 2609 + "concurrently": false, 2610 + "method": "btree", 2611 + "with": {} 2612 + } 2613 + }, 2614 + "foreignKeys": {}, 2615 + "compositePrimaryKeys": {}, 2616 + "uniqueConstraints": {}, 2617 + "policies": {}, 2618 + "checkConstraints": {}, 2619 + "isRLSEnabled": false 2620 + }, 2621 + "public.workflow_rule": { 2622 + "name": "workflow_rule", 2623 + "schema": "", 2624 + "columns": { 2625 + "id": { 2626 + "name": "id", 2627 + "type": "text", 2628 + "primaryKey": true, 2629 + "notNull": true 2630 + }, 2631 + "project_id": { 2632 + "name": "project_id", 2633 + "type": "text", 2634 + "primaryKey": false, 2635 + "notNull": true 2636 + }, 2637 + "integration_type": { 2638 + "name": "integration_type", 2639 + "type": "text", 2640 + "primaryKey": false, 2641 + "notNull": true 2642 + }, 2643 + "event_type": { 2644 + "name": "event_type", 2645 + "type": "text", 2646 + "primaryKey": false, 2647 + "notNull": true 2648 + }, 2649 + "column_id": { 2650 + "name": "column_id", 2651 + "type": "text", 2652 + "primaryKey": false, 2653 + "notNull": true 2654 + }, 2655 + "created_at": { 2656 + "name": "created_at", 2657 + "type": "timestamp", 2658 + "primaryKey": false, 2659 + "notNull": true, 2660 + "default": "now()" 2661 + }, 2662 + "updated_at": { 2663 + "name": "updated_at", 2664 + "type": "timestamp", 2665 + "primaryKey": false, 2666 + "notNull": true, 2667 + "default": "now()" 2668 + } 2669 + }, 2670 + "indexes": { 2671 + "workflow_rule_projectId_idx": { 2672 + "name": "workflow_rule_projectId_idx", 2673 + "columns": [ 2674 + { 2675 + "expression": "project_id", 2676 + "isExpression": false, 2677 + "asc": true, 2678 + "nulls": "last" 2679 + } 2680 + ], 2681 + "isUnique": false, 2682 + "concurrently": false, 2683 + "method": "btree", 2684 + "with": {} 2685 + } 2686 + }, 2687 + "foreignKeys": { 2688 + "workflow_rule_project_id_project_id_fk": { 2689 + "name": "workflow_rule_project_id_project_id_fk", 2690 + "tableFrom": "workflow_rule", 2691 + "tableTo": "project", 2692 + "columnsFrom": ["project_id"], 2693 + "columnsTo": ["id"], 2694 + "onDelete": "cascade", 2695 + "onUpdate": "cascade" 2696 + }, 2697 + "workflow_rule_column_id_column_id_fk": { 2698 + "name": "workflow_rule_column_id_column_id_fk", 2699 + "tableFrom": "workflow_rule", 2700 + "tableTo": "column", 2701 + "columnsFrom": ["column_id"], 2702 + "columnsTo": ["id"], 2703 + "onDelete": "cascade", 2704 + "onUpdate": "cascade" 2705 + } 2706 + }, 2707 + "compositePrimaryKeys": {}, 2708 + "uniqueConstraints": {}, 2709 + "policies": {}, 2710 + "checkConstraints": {}, 2711 + "isRLSEnabled": false 2712 + }, 2713 + "public.workspace": { 2714 + "name": "workspace", 2715 + "schema": "", 2716 + "columns": { 2717 + "id": { 2718 + "name": "id", 2719 + "type": "text", 2720 + "primaryKey": true, 2721 + "notNull": true 2722 + }, 2723 + "name": { 2724 + "name": "name", 2725 + "type": "text", 2726 + "primaryKey": false, 2727 + "notNull": true 2728 + }, 2729 + "slug": { 2730 + "name": "slug", 2731 + "type": "text", 2732 + "primaryKey": false, 2733 + "notNull": true 2734 + }, 2735 + "logo": { 2736 + "name": "logo", 2737 + "type": "text", 2738 + "primaryKey": false, 2739 + "notNull": false 2740 + }, 2741 + "metadata": { 2742 + "name": "metadata", 2743 + "type": "text", 2744 + "primaryKey": false, 2745 + "notNull": false 2746 + }, 2747 + "description": { 2748 + "name": "description", 2749 + "type": "text", 2750 + "primaryKey": false, 2751 + "notNull": false 2752 + }, 2753 + "created_at": { 2754 + "name": "created_at", 2755 + "type": "timestamp", 2756 + "primaryKey": false, 2757 + "notNull": true 2758 + } 2759 + }, 2760 + "indexes": {}, 2761 + "foreignKeys": {}, 2762 + "compositePrimaryKeys": {}, 2763 + "uniqueConstraints": { 2764 + "workspace_slug_unique": { 2765 + "name": "workspace_slug_unique", 2766 + "nullsNotDistinct": false, 2767 + "columns": ["slug"] 2768 + } 2769 + }, 2770 + "policies": {}, 2771 + "checkConstraints": {}, 2772 + "isRLSEnabled": false 2773 + }, 2774 + "public.workspace_member": { 2775 + "name": "workspace_member", 2776 + "schema": "", 2777 + "columns": { 2778 + "id": { 2779 + "name": "id", 2780 + "type": "text", 2781 + "primaryKey": true, 2782 + "notNull": true 2783 + }, 2784 + "workspace_id": { 2785 + "name": "workspace_id", 2786 + "type": "text", 2787 + "primaryKey": false, 2788 + "notNull": true 2789 + }, 2790 + "user_id": { 2791 + "name": "user_id", 2792 + "type": "text", 2793 + "primaryKey": false, 2794 + "notNull": true 2795 + }, 2796 + "role": { 2797 + "name": "role", 2798 + "type": "text", 2799 + "primaryKey": false, 2800 + "notNull": true, 2801 + "default": "'member'" 2802 + }, 2803 + "joined_at": { 2804 + "name": "joined_at", 2805 + "type": "timestamp", 2806 + "primaryKey": false, 2807 + "notNull": true 2808 + } 2809 + }, 2810 + "indexes": { 2811 + "workspace_member_workspaceId_idx": { 2812 + "name": "workspace_member_workspaceId_idx", 2813 + "columns": [ 2814 + { 2815 + "expression": "workspace_id", 2816 + "isExpression": false, 2817 + "asc": true, 2818 + "nulls": "last" 2819 + } 2820 + ], 2821 + "isUnique": false, 2822 + "concurrently": false, 2823 + "method": "btree", 2824 + "with": {} 2825 + }, 2826 + "workspace_member_userId_idx": { 2827 + "name": "workspace_member_userId_idx", 2828 + "columns": [ 2829 + { 2830 + "expression": "user_id", 2831 + "isExpression": false, 2832 + "asc": true, 2833 + "nulls": "last" 2834 + } 2835 + ], 2836 + "isUnique": false, 2837 + "concurrently": false, 2838 + "method": "btree", 2839 + "with": {} 2840 + } 2841 + }, 2842 + "foreignKeys": { 2843 + "workspace_member_workspace_id_workspace_id_fk": { 2844 + "name": "workspace_member_workspace_id_workspace_id_fk", 2845 + "tableFrom": "workspace_member", 2846 + "tableTo": "workspace", 2847 + "columnsFrom": ["workspace_id"], 2848 + "columnsTo": ["id"], 2849 + "onDelete": "cascade", 2850 + "onUpdate": "no action" 2851 + }, 2852 + "workspace_member_user_id_user_id_fk": { 2853 + "name": "workspace_member_user_id_user_id_fk", 2854 + "tableFrom": "workspace_member", 2855 + "tableTo": "user", 2856 + "columnsFrom": ["user_id"], 2857 + "columnsTo": ["id"], 2858 + "onDelete": "cascade", 2859 + "onUpdate": "no action" 2860 + } 2861 + }, 2862 + "compositePrimaryKeys": {}, 2863 + "uniqueConstraints": {}, 2864 + "policies": {}, 2865 + "checkConstraints": {}, 2866 + "isRLSEnabled": false 2867 + } 2868 + }, 2869 + "enums": {}, 2870 + "schemas": {}, 2871 + "sequences": {}, 2872 + "roles": {}, 2873 + "policies": {}, 2874 + "views": {}, 2875 + "_meta": { 2876 + "columns": {}, 2877 + "schemas": {}, 2878 + "tables": {} 2879 + } 2880 + }
+7
apps/api/drizzle/meta/_journal.json
··· 148 148 "when": 1775035770523, 149 149 "tag": "0020_careful_shiver_man", 150 150 "breakpoints": true 151 + }, 152 + { 153 + "idx": 21, 154 + "version": "7", 155 + "when": 1775037521104, 156 + "tag": "0021_tiny_bill_hollister", 157 + "breakpoints": true 151 158 } 152 159 ] 153 160 }
+4
apps/api/src/database/schema.ts
··· 443 443 ntfyServerUrl: text("ntfy_server_url"), 444 444 ntfyTopic: text("ntfy_topic"), 445 445 ntfyToken: text("ntfy_token"), 446 + gotifyEnabled: boolean("gotify_enabled").default(false).notNull(), 447 + gotifyServerUrl: text("gotify_server_url"), 448 + gotifyToken: text("gotify_token"), 446 449 webhookEnabled: boolean("webhook_enabled").default(false).notNull(), 447 450 webhookUrl: text("webhook_url"), 448 451 webhookSecret: text("webhook_secret"), ··· 475 478 isActive: boolean("is_active").default(true).notNull(), 476 479 emailEnabled: boolean("email_enabled").default(false).notNull(), 477 480 ntfyEnabled: boolean("ntfy_enabled").default(false).notNull(), 481 + gotifyEnabled: boolean("gotify_enabled").default(false).notNull(), 478 482 webhookEnabled: boolean("webhook_enabled").default(false).notNull(), 479 483 projectMode: text("project_mode").default("all").notNull(), 480 484 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
+62
apps/api/src/notification-preferences/delivery.ts
··· 215 215 } 216 216 } 217 217 218 + async function sendGotifyNotification(input: { 219 + serverUrl: string; 220 + token: string; 221 + title: string; 222 + body: string; 223 + clickUrl?: string | null; 224 + }) { 225 + await assertPublicWebhookDestination(input.serverUrl); 226 + 227 + const response = await fetch( 228 + `${input.serverUrl.replace(/\/+$/, "")}/message?token=${encodeURIComponent( 229 + input.token, 230 + )}`, 231 + { 232 + method: "POST", 233 + headers: { 234 + "Content-Type": "application/json", 235 + }, 236 + body: JSON.stringify({ 237 + title: input.title, 238 + message: input.body, 239 + priority: 5, 240 + extras: input.clickUrl 241 + ? { 242 + "client::notification": { 243 + click: { 244 + url: input.clickUrl, 245 + }, 246 + }, 247 + "client::display": { 248 + contentType: "text/plain", 249 + }, 250 + } 251 + : undefined, 252 + }), 253 + }, 254 + ); 255 + 256 + if (!response.ok) { 257 + throw new Error( 258 + `Gotify delivery failed (${response.status}): ${await response.text()}`, 259 + ); 260 + } 261 + } 262 + 218 263 async function sendWebhookNotification(input: { 219 264 webhookUrl: string; 220 265 secret?: string | null; ··· 378 423 serverUrl: preference.ntfyServerUrl, 379 424 topic: preference.ntfyTopic, 380 425 token: preference.ntfyToken, 426 + title: content.title, 427 + body: content.body, 428 + clickUrl: context.taskUrl, 429 + }), 430 + ); 431 + } 432 + 433 + if ( 434 + preference.gotifyEnabled && 435 + preference.gotifyServerUrl && 436 + preference.gotifyToken && 437 + rule.gotifyEnabled 438 + ) { 439 + deliveries.push( 440 + sendGotifyNotification({ 441 + serverUrl: preference.gotifyServerUrl, 442 + token: preference.gotifyToken, 381 443 title: content.title, 382 444 body: content.body, 383 445 clickUrl: context.taskUrl,
+4
apps/api/src/notification-preferences/index.ts
··· 13 13 isActive: v.boolean(), 14 14 emailEnabled: v.boolean(), 15 15 ntfyEnabled: v.boolean(), 16 + gotifyEnabled: v.boolean(), 16 17 webhookEnabled: v.boolean(), 17 18 projectMode: v.picklist(["all", "selected"] as const), 18 19 selectedProjectIds: v.optional(v.array(v.string())), ··· 76 77 ntfyServerUrl: v.optional(v.nullable(v.string())), 77 78 ntfyTopic: v.optional(v.nullable(v.string())), 78 79 ntfyToken: v.optional(v.nullable(v.string())), 80 + gotifyEnabled: v.optional(v.boolean()), 81 + gotifyServerUrl: v.optional(v.nullable(v.string())), 82 + gotifyToken: v.optional(v.nullable(v.string())), 79 83 webhookEnabled: v.optional(v.boolean()), 80 84 webhookUrl: v.optional(v.nullable(v.string())), 81 85 webhookSecret: v.optional(v.nullable(v.string())),
+65
apps/api/src/notification-preferences/service.ts
··· 21 21 ntfyTopic: string | null; 22 22 ntfyTokenConfigured: boolean; 23 23 maskedNtfyToken: string | null; 24 + gotifyEnabled: boolean; 25 + gotifyConfigured: boolean; 26 + gotifyServerUrl: string | null; 27 + gotifyTokenConfigured: boolean; 28 + maskedGotifyToken: string | null; 24 29 webhookEnabled: boolean; 25 30 webhookConfigured: boolean; 26 31 webhookUrl: string | null; ··· 33 38 isActive: boolean; 34 39 emailEnabled: boolean; 35 40 ntfyEnabled: boolean; 41 + gotifyEnabled: boolean; 36 42 webhookEnabled: boolean; 37 43 projectMode: NotificationPreferenceProjectMode; 38 44 selectedProjectIds: string[]; ··· 49 55 ntfyServerUrl?: string | null; 50 56 ntfyTopic?: string | null; 51 57 ntfyToken?: string | null; 58 + gotifyEnabled?: boolean; 59 + gotifyServerUrl?: string | null; 60 + gotifyToken?: string | null; 52 61 webhookEnabled?: boolean; 53 62 webhookUrl?: string | null; 54 63 webhookSecret?: string | null; ··· 58 67 isActive: boolean; 59 68 emailEnabled: boolean; 60 69 ntfyEnabled: boolean; 70 + gotifyEnabled: boolean; 61 71 webhookEnabled: boolean; 62 72 projectMode: NotificationPreferenceProjectMode; 63 73 selectedProjectIds?: string[]; ··· 149 159 ntfyTopic: preference?.ntfyTopic ?? null, 150 160 ntfyTokenConfigured: Boolean(preference?.ntfyToken), 151 161 maskedNtfyToken: maskValue(preference?.ntfyToken), 162 + gotifyEnabled: preference?.gotifyEnabled ?? false, 163 + gotifyConfigured: Boolean( 164 + preference?.gotifyServerUrl && preference?.gotifyToken, 165 + ), 166 + gotifyServerUrl: preference?.gotifyServerUrl ?? null, 167 + gotifyTokenConfigured: Boolean(preference?.gotifyToken), 168 + maskedGotifyToken: maskValue(preference?.gotifyToken), 152 169 webhookEnabled: preference?.webhookEnabled ?? false, 153 170 webhookConfigured: Boolean(preference?.webhookUrl), 154 171 webhookUrl: preference?.webhookUrl ?? null, ··· 161 178 isActive: rule.isActive ?? true, 162 179 emailEnabled: rule.emailEnabled ?? false, 163 180 ntfyEnabled: rule.ntfyEnabled ?? false, 181 + gotifyEnabled: rule.gotifyEnabled ?? false, 164 182 webhookEnabled: rule.webhookEnabled ?? false, 165 183 projectMode: 166 184 rule.projectMode === "selected" ? "selected" : ("all" as const), ··· 191 209 input.ntfyTopic ?? existing?.ntfyTopic, 192 210 ); 193 211 const ntfyToken = normalizeOptionalString(input.ntfyToken ?? undefined); 212 + const gotifyServerUrl = normalizeOptionalString( 213 + input.gotifyServerUrl ?? existing?.gotifyServerUrl, 214 + ); 215 + const gotifyToken = normalizeOptionalString(input.gotifyToken ?? undefined); 194 216 const webhookUrl = normalizeOptionalString( 195 217 input.webhookUrl ?? existing?.webhookUrl, 196 218 ); ··· 200 222 201 223 const emailEnabled = input.emailEnabled ?? existing?.emailEnabled ?? false; 202 224 const ntfyEnabled = input.ntfyEnabled ?? existing?.ntfyEnabled ?? false; 225 + const gotifyEnabled = input.gotifyEnabled ?? existing?.gotifyEnabled ?? false; 203 226 const webhookEnabled = 204 227 input.webhookEnabled ?? existing?.webhookEnabled ?? false; 205 228 ··· 227 250 } 228 251 } 229 252 253 + if (gotifyEnabled || gotifyServerUrl || gotifyToken !== undefined) { 254 + if (!gotifyServerUrl || !gotifyToken) { 255 + throw new HTTPException(400, { 256 + message: "Gotify requires a server URL and app token", 257 + }); 258 + } 259 + 260 + try { 261 + new URL(gotifyServerUrl); 262 + await assertPublicWebhookDestination(gotifyServerUrl); 263 + } catch (error) { 264 + throw new HTTPException(400, { 265 + message: 266 + error instanceof Error ? error.message : "Invalid Gotify server URL", 267 + }); 268 + } 269 + } 270 + 230 271 if (webhookEnabled || webhookUrl || webhookSecret !== undefined) { 231 272 if (!webhookUrl) { 232 273 throw new HTTPException(400, { ··· 252 293 ntfyTopic, 253 294 ntfyToken: 254 295 ntfyToken === undefined ? (existing?.ntfyToken ?? null) : ntfyToken, 296 + gotifyEnabled, 297 + gotifyServerUrl, 298 + gotifyToken: 299 + gotifyToken === undefined ? (existing?.gotifyToken ?? null) : gotifyToken, 255 300 webhookEnabled, 256 301 webhookUrl, 257 302 webhookSecret: ··· 286 331 .where(eq(userNotificationWorkspaceRuleTable.userId, userId)); 287 332 } 288 333 334 + if (!gotifyEnabled || !gotifyServerUrl || !data.gotifyToken) { 335 + await db 336 + .update(userNotificationWorkspaceRuleTable) 337 + .set({ gotifyEnabled: false, updatedAt: new Date() }) 338 + .where(eq(userNotificationWorkspaceRuleTable.userId, userId)); 339 + } 340 + 289 341 if (!webhookEnabled || !webhookUrl) { 290 342 await db 291 343 .update(userNotificationWorkspaceRuleTable) ··· 338 390 }); 339 391 } 340 392 393 + if ( 394 + input.gotifyEnabled && 395 + (!preference?.gotifyEnabled || 396 + !preference.gotifyServerUrl || 397 + !preference.gotifyToken) 398 + ) { 399 + throw new HTTPException(400, { 400 + message: "Enable Gotify notifications globally before using them here", 401 + }); 402 + } 403 + 341 404 const existing = await db.query.userNotificationWorkspaceRuleTable.findFirst({ 342 405 where: and( 343 406 eq(userNotificationWorkspaceRuleTable.userId, userId), ··· 354 417 isActive: input.isActive, 355 418 emailEnabled: input.emailEnabled, 356 419 ntfyEnabled: input.ntfyEnabled, 420 + gotifyEnabled: input.gotifyEnabled, 357 421 webhookEnabled: input.webhookEnabled, 358 422 projectMode: input.projectMode, 359 423 updatedAt: new Date(), ··· 368 432 isActive: input.isActive, 369 433 emailEnabled: input.emailEnabled, 370 434 ntfyEnabled: input.ntfyEnabled, 435 + gotifyEnabled: input.gotifyEnabled, 371 436 webhookEnabled: input.webhookEnabled, 372 437 projectMode: input.projectMode, 373 438 })
+6
apps/api/src/schemas.ts
··· 105 105 isActive: v.boolean(), 106 106 emailEnabled: v.boolean(), 107 107 ntfyEnabled: v.boolean(), 108 + gotifyEnabled: v.boolean(), 108 109 webhookEnabled: v.boolean(), 109 110 projectMode: v.picklist(["all", "selected"] as const), 110 111 selectedProjectIds: v.array(v.string()), ··· 121 122 ntfyTopic: v.nullable(v.string()), 122 123 ntfyTokenConfigured: v.boolean(), 123 124 maskedNtfyToken: v.nullable(v.string()), 125 + gotifyEnabled: v.boolean(), 126 + gotifyConfigured: v.boolean(), 127 + gotifyServerUrl: v.nullable(v.string()), 128 + gotifyTokenConfigured: v.boolean(), 129 + maskedGotifyToken: v.nullable(v.string()), 124 130 webhookEnabled: v.boolean(), 125 131 webhookConfigured: v.boolean(), 126 132 webhookUrl: v.nullable(v.string()),
+156
apps/web/src/components/account/notification-preferences-settings.tsx
··· 27 27 isActive: boolean; 28 28 emailEnabled: boolean; 29 29 ntfyEnabled: boolean; 30 + gotifyEnabled: boolean; 30 31 webhookEnabled: boolean; 31 32 projectMode: "all" | "selected"; 32 33 selectedProjectIds: string[]; ··· 62 63 63 64 function WorkspaceRuleCard({ 64 65 hasEmailChannel, 66 + hasGotifyChannel, 65 67 hasNtfyChannel, 66 68 hasWebhookChannel, 67 69 onDelete, ··· 70 72 workspace, 71 73 }: { 72 74 hasEmailChannel: boolean; 75 + hasGotifyChannel: boolean; 73 76 hasNtfyChannel: boolean; 74 77 hasWebhookChannel: boolean; 75 78 onDelete: (workspaceId: string) => Promise<unknown>; ··· 78 81 isActive: boolean; 79 82 emailEnabled: boolean; 80 83 ntfyEnabled: boolean; 84 + gotifyEnabled: boolean; 81 85 webhookEnabled: boolean; 82 86 projectMode: "all" | "selected"; 83 87 selectedProjectIds: string[]; ··· 89 93 isActive: rule?.isActive ?? false, 90 94 emailEnabled: rule?.emailEnabled ?? false, 91 95 ntfyEnabled: rule?.ntfyEnabled ?? false, 96 + gotifyEnabled: rule?.gotifyEnabled ?? false, 92 97 webhookEnabled: rule?.webhookEnabled ?? false, 93 98 projectMode: rule?.projectMode ?? "all", 94 99 selectedProjectIds: rule?.selectedProjectIds ?? [], ··· 108 113 isActive: rule?.isActive ?? false, 109 114 emailEnabled: rule?.emailEnabled ?? false, 110 115 ntfyEnabled: rule?.ntfyEnabled ?? false, 116 + gotifyEnabled: rule?.gotifyEnabled ?? false, 111 117 webhookEnabled: rule?.webhookEnabled ?? false, 112 118 projectMode: rule?.projectMode ?? "all", 113 119 selectedProjectIds: rule?.selectedProjectIds ?? [], ··· 184 190 label={t("settings:notificationsPage.workspaceCardLabelNtfy")} 185 191 onCheckedChange={(checked) => 186 192 setState((current) => ({ ...current, ntfyEnabled: checked })) 193 + } 194 + /> 195 + <ChannelToggle 196 + checked={state.gotifyEnabled} 197 + disabled={!hasGotifyChannel || isBusy} 198 + hint={ 199 + hasGotifyChannel 200 + ? t("settings:notificationsPage.gotifyChannelHintEnabled") 201 + : t("settings:notificationsPage.gotifyChannelHintDisabled") 202 + } 203 + label={t("settings:notificationsPage.workspaceCardLabelGotify")} 204 + onCheckedChange={(checked) => 205 + setState((current) => ({ ...current, gotifyEnabled: checked })) 187 206 } 188 207 /> 189 208 <ChannelToggle ··· 383 402 const [ntfyServerUrl, setNtfyServerUrl] = React.useState(""); 384 403 const [ntfyTopic, setNtfyTopic] = React.useState(""); 385 404 const [ntfyToken, setNtfyToken] = React.useState(""); 405 + const [gotifyEnabled, setGotifyEnabled] = React.useState(false); 406 + const [gotifyServerUrl, setGotifyServerUrl] = React.useState(""); 407 + const [gotifyToken, setGotifyToken] = React.useState(""); 386 408 const [webhookEnabled, setWebhookEnabled] = React.useState(false); 387 409 const [webhookUrl, setWebhookUrl] = React.useState(""); 388 410 const [webhookSecret, setWebhookSecret] = React.useState(""); ··· 394 416 setNtfyServerUrl(preferences.ntfyServerUrl ?? ""); 395 417 setNtfyTopic(preferences.ntfyTopic ?? ""); 396 418 setNtfyToken(""); 419 + setGotifyEnabled(preferences.gotifyEnabled); 420 + setGotifyServerUrl(preferences.gotifyServerUrl ?? ""); 421 + setGotifyToken(""); 397 422 setWebhookEnabled(preferences.webhookEnabled); 398 423 setWebhookUrl(preferences.webhookUrl ?? ""); 399 424 setWebhookSecret(""); ··· 490 515 <div className="flex items-start justify-between gap-4"> 491 516 <div className="space-y-1"> 492 517 <h3 className="font-medium"> 518 + {t("settings:notificationsPage.gotifyTitle")} 519 + </h3> 520 + <p className="text-sm text-muted-foreground"> 521 + {t("settings:notificationsPage.gotifyDescription")} 522 + </p> 523 + </div> 524 + <div className="flex items-center gap-3"> 525 + {preferences?.gotifyConfigured ? ( 526 + <div className="flex items-center gap-2 text-sm text-muted-foreground"> 527 + <CheckCircle className="size-4 text-green-600" /> 528 + <span> 529 + {preferences.gotifyEnabled 530 + ? t("settings:notificationsPage.statusConnected") 531 + : t("settings:notificationsPage.statusPaused")} 532 + </span> 533 + </div> 534 + ) : null} 535 + <Switch 536 + checked={gotifyEnabled} 537 + disabled={isSavingPreferences} 538 + onCheckedChange={setGotifyEnabled} 539 + /> 540 + </div> 541 + </div> 542 + 543 + <div className="space-y-4"> 544 + <div className="space-y-1"> 545 + <Label className="text-sm font-medium"> 546 + {t("settings:notificationsPage.serverUrl")} 547 + </Label> 548 + <Input 549 + autoComplete="off" 550 + placeholder={t( 551 + "settings:notificationsPage.gotifyServerPlaceholder", 552 + )} 553 + value={gotifyServerUrl} 554 + onChange={(event) => setGotifyServerUrl(event.target.value)} 555 + /> 556 + </div> 557 + <div className="space-y-1"> 558 + <Label className="text-sm font-medium"> 559 + {t("settings:notificationsPage.gotifyTokenLabel")} 560 + </Label> 561 + <Input 562 + autoComplete="off" 563 + placeholder={t( 564 + "settings:notificationsPage.gotifyTokenPlaceholder", 565 + )} 566 + type="password" 567 + value={gotifyToken} 568 + onChange={(event) => setGotifyToken(event.target.value)} 569 + /> 570 + <p className="text-xs text-muted-foreground"> 571 + {preferences?.gotifyTokenConfigured 572 + ? t("settings:notificationsPage.gotifyTokenHintConfigured", { 573 + masked: preferences.maskedGotifyToken ?? "••••", 574 + }) 575 + : t("settings:notificationsPage.gotifyTokenHintRequired")} 576 + </p> 577 + </div> 578 + </div> 579 + 580 + <div className="flex flex-wrap gap-2"> 581 + <Button 582 + disabled={isSavingPreferences} 583 + onClick={async () => { 584 + try { 585 + await updatePreferences({ 586 + gotifyEnabled, 587 + gotifyServerUrl, 588 + gotifyToken: gotifyToken.trim() ? gotifyToken : undefined, 589 + }); 590 + setGotifyToken(""); 591 + toast.success(t("settings:notificationsPage.toastGotifySaved")); 592 + } catch (error) { 593 + toast.error( 594 + error instanceof Error 595 + ? error.message 596 + : t("settings:notificationsPage.toastGotifySaveFailed"), 597 + ); 598 + } 599 + }} 600 + type="button" 601 + > 602 + {preferences?.gotifyConfigured 603 + ? t("settings:notificationsPage.saveChanges") 604 + : t("settings:notificationsPage.connectGotify")} 605 + </Button> 606 + {preferences?.gotifyConfigured ? ( 607 + <Button 608 + disabled={isSavingPreferences} 609 + onClick={async () => { 610 + try { 611 + await updatePreferences({ 612 + gotifyEnabled: false, 613 + gotifyServerUrl: null, 614 + gotifyToken: null, 615 + }); 616 + setGotifyEnabled(false); 617 + setGotifyServerUrl(""); 618 + setGotifyToken(""); 619 + toast.success( 620 + t("settings:notificationsPage.toastGotifyDisconnected"), 621 + ); 622 + } catch (error) { 623 + toast.error( 624 + error instanceof Error 625 + ? error.message 626 + : t( 627 + "settings:notificationsPage.toastGotifyDisconnectFailed", 628 + ), 629 + ); 630 + } 631 + }} 632 + type="button" 633 + variant="outline" 634 + > 635 + <Trash2 className="size-4" /> 636 + {t("settings:notificationsPage.disconnect")} 637 + </Button> 638 + ) : null} 639 + </div> 640 + </div> 641 + 642 + <div className="space-y-4 rounded-md border border-border bg-sidebar p-4"> 643 + <div className="flex items-start justify-between gap-4"> 644 + <div className="space-y-1"> 645 + <h3 className="font-medium"> 493 646 {t("settings:notificationsPage.ntfyTitle")} 494 647 </h3> 495 648 <p className="text-sm text-muted-foreground"> ··· 777 930 <WorkspaceRuleCard 778 931 key={workspace.id} 779 932 hasEmailChannel={Boolean(preferences?.emailEnabled)} 933 + hasGotifyChannel={Boolean( 934 + preferences?.gotifyEnabled && preferences?.gotifyConfigured, 935 + )} 780 936 hasNtfyChannel={Boolean( 781 937 preferences?.ntfyEnabled && preferences?.ntfyConfigured, 782 938 )}
+6
apps/web/src/fetchers/notification-preferences/get-notification-preferences.ts
··· 7 7 isActive: boolean; 8 8 emailEnabled: boolean; 9 9 ntfyEnabled: boolean; 10 + gotifyEnabled: boolean; 10 11 webhookEnabled: boolean; 11 12 projectMode: "all" | "selected"; 12 13 selectedProjectIds: string[]; ··· 23 24 ntfyTopic: string | null; 24 25 ntfyTokenConfigured: boolean; 25 26 maskedNtfyToken: string | null; 27 + gotifyEnabled: boolean; 28 + gotifyConfigured: boolean; 29 + gotifyServerUrl: string | null; 30 + gotifyTokenConfigured: boolean; 31 + maskedGotifyToken: string | null; 26 32 webhookEnabled: boolean; 27 33 webhookConfigured: boolean; 28 34 webhookUrl: string | null;
+3
apps/web/src/fetchers/notification-preferences/update-notification-preferences.ts
··· 7 7 ntfyServerUrl?: string | null; 8 8 ntfyTopic?: string | null; 9 9 ntfyToken?: string | null; 10 + gotifyEnabled?: boolean; 11 + gotifyServerUrl?: string | null; 12 + gotifyToken?: string | null; 10 13 webhookEnabled?: boolean; 11 14 webhookUrl?: string | null; 12 15 webhookSecret?: string | null;
+1
apps/web/src/fetchers/notification-preferences/upsert-notification-workspace-rule.ts
··· 5 5 isActive: boolean; 6 6 emailEnabled: boolean; 7 7 ntfyEnabled: boolean; 8 + gotifyEnabled: boolean; 8 9 webhookEnabled: boolean; 9 10 projectMode: "all" | "selected"; 10 11 selectedProjectIds?: string[];
+15
i18n/de-DE.json
··· 344 344 "ntfyTokenHintConfigured": "Ein Token ist bereits konfiguriert ({{masked}}). Gib einen neuen Token ein, um ihn zu ersetzen.", 345 345 "ntfyTokenHintOptional": "Optional. Gib einen Token an, wenn dein ntfy-Server eine Authentifizierung verlangt.", 346 346 "connectNtfy": "ntfy verbinden", 347 + "gotifyTitle": "Gotify", 348 + "gotifyDescription": "Sende Kontobenachrichtigungen an deinen Gotify-Server.", 349 + "gotifyTokenLabel": "App-Token", 350 + "gotifyServerPlaceholder": "https://gotify.example.com", 351 + "gotifyTokenPlaceholder": "Gotify-App-Token", 352 + "gotifyTokenHintConfigured": "Ein App-Token ist bereits konfiguriert ({{masked}}). Gib ein neues Token ein, um es zu ersetzen.", 353 + "gotifyTokenHintRequired": "Erforderlich. Verwende ein Anwendungstoken von deinem Gotify-Server.", 354 + "connectGotify": "Gotify verbinden", 347 355 "webhookTitle": "Benutzerdefinierter Webhook", 348 356 "webhookDescription": "Kontobenachrichtigungen als JSON an deinen eigenen Endpunkt senden.", 349 357 "endpointUrl": "Endpunkt-URL", ··· 358 366 "workspaceCardHint": "Wähle, welche Kanäle dieser Arbeitsbereich für Kontobenachrichtigungen nutzen darf.", 359 367 "workspaceCardLabelEmail": "E-Mail", 360 368 "workspaceCardLabelNtfy": "ntfy", 369 + "workspaceCardLabelGotify": "Gotify", 361 370 "workspaceCardLabelWebhook": "Benutzerdefinierter Webhook", 362 371 "emailChannelHintEnabled": "Passende Arbeitsbereichs-Benachrichtigungen per E-Mail senden.", 363 372 "emailChannelHintDisabled": "E-Mail zuerst global konfigurieren und aktivieren.", 364 373 "ntfyChannelHintEnabled": "Passende Arbeitsbereichs-Benachrichtigungen an ntfy senden.", 365 374 "ntfyChannelHintDisabled": "ntfy zuerst global konfigurieren und aktivieren.", 375 + "gotifyChannelHintEnabled": "Passende Arbeitsbereichs-Benachrichtigungen an Gotify senden.", 376 + "gotifyChannelHintDisabled": "Gotify zuerst global konfigurieren und aktivieren.", 366 377 "webhookChannelHintEnabled": "Passende Arbeitsbereichs-Benachrichtigungen an deinen Webhook senden.", 367 378 "webhookChannelHintDisabled": "Webhook zuerst global konfigurieren und aktivieren.", 368 379 "projectScope": "Projektbereich", ··· 380 391 "toastNtfySaveFailed": "ntfy-Einstellungen konnten nicht gespeichert werden", 381 392 "toastNtfyDisconnected": "ntfy getrennt", 382 393 "toastNtfyDisconnectFailed": "ntfy konnte nicht getrennt werden", 394 + "toastGotifySaved": "Gotify-Einstellungen gespeichert", 395 + "toastGotifySaveFailed": "Gotify-Einstellungen konnten nicht gespeichert werden", 396 + "toastGotifyDisconnected": "Gotify getrennt", 397 + "toastGotifyDisconnectFailed": "Gotify konnte nicht getrennt werden", 383 398 "toastWebhookSaved": "Webhook-Einstellungen gespeichert", 384 399 "toastWebhookSaveFailed": "Webhook-Einstellungen konnten nicht gespeichert werden", 385 400 "toastWebhookDisconnected": "Webhook getrennt",
+15
i18n/el-GR.json
··· 344 344 "ntfyTokenHintConfigured": "Ένα διακριτικό έχει ήδη ρυθμιστεί ({{masked}}). Εισαγάγετε νέο για αντικατάσταση.", 345 345 "ntfyTokenHintOptional": "Προαιρετικό. Δώστε διακριτικό αν ο διακομιστής ntfy απαιτεί πιστοποίηση.", 346 346 "connectNtfy": "Σύνδεση ntfy", 347 + "gotifyTitle": "Gotify", 348 + "gotifyDescription": "Στείλτε ειδοποιήσεις λογαριασμού στον διακομιστή Gotify σας.", 349 + "gotifyTokenLabel": "Διακριτικό εφαρμογής", 350 + "gotifyServerPlaceholder": "https://gotify.example.com", 351 + "gotifyTokenPlaceholder": "Διακριτικό εφαρμογής Gotify", 352 + "gotifyTokenHintConfigured": "Ένα διακριτικό εφαρμογής έχει ήδη ρυθμιστεί ({{masked}}). Εισαγάγετε νέο για αντικατάσταση.", 353 + "gotifyTokenHintRequired": "Απαιτείται. Χρησιμοποιήστε διακριτικό εφαρμογής από τον διακομιστή Gotify σας.", 354 + "connectGotify": "Σύνδεση Gotify", 347 355 "webhookTitle": "Προσαρμοσμένο webhook", 348 356 "webhookDescription": "Στείλτε ειδοποιήσεις λογαριασμού στο δικό σας endpoint ως JSON.", 349 357 "endpointUrl": "URL τελικού σημείου", ··· 358 366 "workspaceCardHint": "Επιλέξτε ποια κανάλια μπορεί να χρησιμοποιεί αυτός ο χώρος εργασίας για ειδοποιήσεις λογαριασμού.", 359 367 "workspaceCardLabelEmail": "Email", 360 368 "workspaceCardLabelNtfy": "ntfy", 369 + "workspaceCardLabelGotify": "Gotify", 361 370 "workspaceCardLabelWebhook": "Προσαρμοσμένο webhook", 362 371 "emailChannelHintEnabled": "Αποστολή αντίστοιχων ειδοποιήσεων χώρου εργασίας μέσω email.", 363 372 "emailChannelHintDisabled": "Ρυθμίστε και ενεργοποιήστε πρώτα το email καθολικά.", 364 373 "ntfyChannelHintEnabled": "Αποστολή αντίστοιχων ειδοποιήσεων χώρου εργασίας στο ntfy.", 365 374 "ntfyChannelHintDisabled": "Ρυθμίστε και ενεργοποιήστε πρώτα το ntfy καθολικά.", 375 + "gotifyChannelHintEnabled": "Αποστολή αντίστοιχων ειδοποιήσεων χώρου εργασίας στο Gotify.", 376 + "gotifyChannelHintDisabled": "Ρυθμίστε και ενεργοποιήστε πρώτα το Gotify καθολικά.", 366 377 "webhookChannelHintEnabled": "Αποστολή αντίστοιχων ειδοποιήσεων χώρου εργασίας στο webhook σας.", 367 378 "webhookChannelHintDisabled": "Ρυθμίστε και ενεργοποιήστε πρώτα το webhook καθολικά.", 368 379 "projectScope": "Εύρος έργων", ··· 380 391 "toastNtfySaveFailed": "Αποτυχία αποθήκευσης ρυθμίσεων ntfy", 381 392 "toastNtfyDisconnected": "Το ntfy αποσυνδέθηκε", 382 393 "toastNtfyDisconnectFailed": "Αποτυχία αποσύνδεσης ntfy", 394 + "toastGotifySaved": "Οι ρυθμίσεις Gotify αποθηκεύτηκαν", 395 + "toastGotifySaveFailed": "Αποτυχία αποθήκευσης ρυθμίσεων Gotify", 396 + "toastGotifyDisconnected": "Το Gotify αποσυνδέθηκε", 397 + "toastGotifyDisconnectFailed": "Αποτυχία αποσύνδεσης Gotify", 383 398 "toastWebhookSaved": "Οι ρυθμίσεις webhook αποθηκεύτηκαν", 384 399 "toastWebhookSaveFailed": "Αποτυχία αποθήκευσης ρυθμίσεων webhook", 385 400 "toastWebhookDisconnected": "Το webhook αποσυνδέθηκε",
+15
i18n/en-US.json
··· 344 344 "ntfyTokenHintConfigured": "A token is already configured ({{masked}}). Enter a new token to replace it.", 345 345 "ntfyTokenHintOptional": "Optional. Provide a token if your ntfy server requires authentication.", 346 346 "connectNtfy": "Connect ntfy", 347 + "gotifyTitle": "Gotify", 348 + "gotifyDescription": "Send account notifications to your Gotify server.", 349 + "gotifyTokenLabel": "App token", 350 + "gotifyServerPlaceholder": "https://gotify.example.com", 351 + "gotifyTokenPlaceholder": "Gotify app token", 352 + "gotifyTokenHintConfigured": "An app token is already configured ({{masked}}). Enter a new token to replace it.", 353 + "gotifyTokenHintRequired": "Required. Use an application token from your Gotify server.", 354 + "connectGotify": "Connect Gotify", 347 355 "webhookTitle": "Custom webhook", 348 356 "webhookDescription": "Send account notifications to your own endpoint as JSON.", 349 357 "endpointUrl": "Endpoint URL", ··· 358 366 "workspaceCardHint": "Choose which channels this workspace can use for account notifications.", 359 367 "workspaceCardLabelEmail": "Email", 360 368 "workspaceCardLabelNtfy": "ntfy", 369 + "workspaceCardLabelGotify": "Gotify", 361 370 "workspaceCardLabelWebhook": "Custom webhook", 362 371 "emailChannelHintEnabled": "Send matching workspace notifications by email.", 363 372 "emailChannelHintDisabled": "Configure and enable email globally first.", 364 373 "ntfyChannelHintEnabled": "Send matching workspace notifications to ntfy.", 365 374 "ntfyChannelHintDisabled": "Configure and enable ntfy globally first.", 375 + "gotifyChannelHintEnabled": "Send matching workspace notifications to Gotify.", 376 + "gotifyChannelHintDisabled": "Configure and enable Gotify globally first.", 366 377 "webhookChannelHintEnabled": "Send matching workspace notifications to your webhook.", 367 378 "webhookChannelHintDisabled": "Configure and enable the webhook globally first.", 368 379 "projectScope": "Project scope", ··· 380 391 "toastNtfySaveFailed": "Failed to save ntfy settings", 381 392 "toastNtfyDisconnected": "ntfy disconnected", 382 393 "toastNtfyDisconnectFailed": "Failed to disconnect ntfy", 394 + "toastGotifySaved": "Gotify settings saved", 395 + "toastGotifySaveFailed": "Failed to save Gotify settings", 396 + "toastGotifyDisconnected": "Gotify disconnected", 397 + "toastGotifyDisconnectFailed": "Failed to disconnect Gotify", 383 398 "toastWebhookSaved": "Webhook settings saved", 384 399 "toastWebhookSaveFailed": "Failed to save webhook settings", 385 400 "toastWebhookDisconnected": "Webhook disconnected",
+15
i18n/fr-FR.json
··· 344 344 "ntfyTokenHintConfigured": "Un jeton est déjà configuré ({{masked}}). Saisissez-en un nouveau pour le remplacer.", 345 345 "ntfyTokenHintOptional": "Facultatif. Indiquez un jeton si votre serveur ntfy exige une authentification.", 346 346 "connectNtfy": "Connecter ntfy", 347 + "gotifyTitle": "Gotify", 348 + "gotifyDescription": "Envoyer les notifications de compte vers votre serveur Gotify.", 349 + "gotifyTokenLabel": "Jeton d’application", 350 + "gotifyServerPlaceholder": "https://gotify.example.com", 351 + "gotifyTokenPlaceholder": "Jeton d’application Gotify", 352 + "gotifyTokenHintConfigured": "Un jeton d’application est déjà configuré ({{masked}}). Saisissez-en un nouveau pour le remplacer.", 353 + "gotifyTokenHintRequired": "Obligatoire. Utilisez un jeton d’application de votre serveur Gotify.", 354 + "connectGotify": "Connecter Gotify", 347 355 "webhookTitle": "Webhook personnalisé", 348 356 "webhookDescription": "Envoyer les notifications de compte vers votre propre point de terminaison en JSON.", 349 357 "endpointUrl": "URL du point de terminaison", ··· 358 366 "workspaceCardHint": "Choisissez les canaux que cet espace de travail peut utiliser pour les notifications de compte.", 359 367 "workspaceCardLabelEmail": "E-mail", 360 368 "workspaceCardLabelNtfy": "ntfy", 369 + "workspaceCardLabelGotify": "Gotify", 361 370 "workspaceCardLabelWebhook": "Webhook personnalisé", 362 371 "emailChannelHintEnabled": "Envoyer les notifications d’espace de travail correspondantes par e-mail.", 363 372 "emailChannelHintDisabled": "Configurez et activez d’abord l’e-mail globalement.", 364 373 "ntfyChannelHintEnabled": "Envoyer les notifications d’espace de travail correspondantes vers ntfy.", 365 374 "ntfyChannelHintDisabled": "Configurez et activez d’abord ntfy globalement.", 375 + "gotifyChannelHintEnabled": "Envoyer les notifications d’espace de travail correspondantes vers Gotify.", 376 + "gotifyChannelHintDisabled": "Configurez et activez d’abord Gotify globalement.", 366 377 "webhookChannelHintEnabled": "Envoyer les notifications d’espace de travail correspondantes vers votre webhook.", 367 378 "webhookChannelHintDisabled": "Configurez et activez d’abord le webhook globalement.", 368 379 "projectScope": "Portée des projets", ··· 380 391 "toastNtfySaveFailed": "Échec de l’enregistrement des paramètres ntfy", 381 392 "toastNtfyDisconnected": "ntfy déconnecté", 382 393 "toastNtfyDisconnectFailed": "Échec de la déconnexion de ntfy", 394 + "toastGotifySaved": "Paramètres Gotify enregistrés", 395 + "toastGotifySaveFailed": "Échec de l’enregistrement des paramètres Gotify", 396 + "toastGotifyDisconnected": "Gotify déconnecté", 397 + "toastGotifyDisconnectFailed": "Échec de la déconnexion de Gotify", 383 398 "toastWebhookSaved": "Paramètres du webhook enregistrés", 384 399 "toastWebhookSaveFailed": "Échec de l’enregistrement des paramètres du webhook", 385 400 "toastWebhookDisconnected": "Webhook déconnecté",
+15
i18n/mk-MK.json
··· 344 344 "ntfyTokenHintConfigured": "Токенот веќе е конфигуриран ({{masked}}). Внеси нов за да го замениш.", 345 345 "ntfyTokenHintOptional": "Опционално. Дај токен ако ntfy серверот бара автентикација.", 346 346 "connectNtfy": "Поврзи ntfy", 347 + "gotifyTitle": "Gotify", 348 + "gotifyDescription": "Испрати известувања за сметката до твојот Gotify сервер.", 349 + "gotifyTokenLabel": "Апликациски токен", 350 + "gotifyServerPlaceholder": "https://gotify.example.com", 351 + "gotifyTokenPlaceholder": "Gotify апликациски токен", 352 + "gotifyTokenHintConfigured": "Апликациски токен веќе е конфигуриран ({{masked}}). Внеси нов за да го замениш.", 353 + "gotifyTokenHintRequired": "Задолжително. Користи апликациски токен од твојот Gotify сервер.", 354 + "connectGotify": "Поврзи Gotify", 347 355 "webhookTitle": "Прилагоден webhook", 348 356 "webhookDescription": "Испрати известувања за сметката до сопствениот endpoint како JSON.", 349 357 "endpointUrl": "URL на endpoint", ··· 358 366 "workspaceCardHint": "Одбери кои канали овој работен простор може да ги користи за известувања за сметката.", 359 367 "workspaceCardLabelEmail": "Е-пошта", 360 368 "workspaceCardLabelNtfy": "ntfy", 369 + "workspaceCardLabelGotify": "Gotify", 361 370 "workspaceCardLabelWebhook": "Прилагоден webhook", 362 371 "emailChannelHintEnabled": "Испрати соодветни известувања за работниот простор по е-пошта.", 363 372 "emailChannelHintDisabled": "Прво конфигурирај и овозможи е-пошта глобално.", 364 373 "ntfyChannelHintEnabled": "Испрати соодветни известувања за работниот простор до ntfy.", 365 374 "ntfyChannelHintDisabled": "Прво конфигурирај и овозможи ntfy глобално.", 375 + "gotifyChannelHintEnabled": "Испрати соодветни известувања за работниот простор до Gotify.", 376 + "gotifyChannelHintDisabled": "Прво конфигурирај и овозможи Gotify глобално.", 366 377 "webhookChannelHintEnabled": "Испрати соодветни известувања за работниот простор до твојот webhook.", 367 378 "webhookChannelHintDisabled": "Прво конфигурирај и овозможи webhook глобално.", 368 379 "projectScope": "Опсег на проекти", ··· 380 391 "toastNtfySaveFailed": "Не успеа зачувувањето на ntfy поставките", 381 392 "toastNtfyDisconnected": "ntfy е исклучен", 382 393 "toastNtfyDisconnectFailed": "Не успеа исклучувањето на ntfy", 394 + "toastGotifySaved": "Зачувани се Gotify поставките", 395 + "toastGotifySaveFailed": "Не успеа зачувувањето на Gotify поставките", 396 + "toastGotifyDisconnected": "Gotify е исклучен", 397 + "toastGotifyDisconnectFailed": "Не успеа исклучувањето на Gotify", 383 398 "toastWebhookSaved": "Зачувани се webhook поставките", 384 399 "toastWebhookSaveFailed": "Не успеа зачувувањето на webhook поставките", 385 400 "toastWebhookDisconnected": "Webhook е исклучен",