Exosphere is a set of small, modular, self-hostable community tools built on the AT Protocol. app.exosphere.site
6
fork

Configure Feed

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

perf: dashboard improvements

Hugo bd008822 c6d51d03

+2024 -41
+1 -1
drizzle/0010_dark_red_skull.sql
··· 1 - CREATE TABLE `did_handles` ( 1 + CREATE TABLE IF NOT EXISTS `did_handles` ( 2 2 `did` text PRIMARY KEY NOT NULL, 3 3 `handle` text NOT NULL, 4 4 `updated_at` text DEFAULT (datetime('now')) NOT NULL
+10
drizzle/0011_military_diamondback.sql
··· 1 + DROP INDEX IF EXISTS `idx_feature_requests_sphere`;--> statement-breakpoint 2 + DROP INDEX IF EXISTS `idx_feature_requests_status`;--> statement-breakpoint 3 + -- Safety: an earlier iteration of this migration created a 2-column 4 + -- `(sphere_id, hidden_at)` index here. The 3-column composite below supersedes 5 + -- it; drop it if present so no orphan index remains on DBs that applied it. 6 + DROP INDEX IF EXISTS `idx_feature_requests_sphere_hidden`;--> statement-breakpoint 7 + CREATE INDEX IF NOT EXISTS `idx_feature_requests_sphere_hidden_status` ON `feature_requests` (`sphere_id`,`hidden_at`,`status`);--> statement-breakpoint 8 + DROP INDEX IF EXISTS `idx_kanban_tasks_sphere`;--> statement-breakpoint 9 + DROP INDEX IF EXISTS `idx_kanban_tasks_status`;--> statement-breakpoint 10 + CREATE INDEX IF NOT EXISTS `idx_kanban_tasks_sphere_hidden` ON `kanban_tasks` (`sphere_id`,`hidden_at`);
+1599
drizzle/meta/0011_snapshot.json
··· 1 + { 2 + "version": "6", 3 + "dialect": "sqlite", 4 + "id": "e31ac405-322f-439b-aec2-d8f775b4b2f8", 5 + "prevId": "8a2b0a3b-d03a-466d-b73f-141810627773", 6 + "tables": { 7 + "oauth_sessions": { 8 + "name": "oauth_sessions", 9 + "columns": { 10 + "key": { 11 + "name": "key", 12 + "type": "text", 13 + "primaryKey": true, 14 + "notNull": true, 15 + "autoincrement": false 16 + }, 17 + "session": { 18 + "name": "session", 19 + "type": "text", 20 + "primaryKey": false, 21 + "notNull": true, 22 + "autoincrement": false 23 + }, 24 + "created_at": { 25 + "name": "created_at", 26 + "type": "text", 27 + "primaryKey": false, 28 + "notNull": true, 29 + "autoincrement": false, 30 + "default": "(datetime('now'))" 31 + }, 32 + "updated_at": { 33 + "name": "updated_at", 34 + "type": "text", 35 + "primaryKey": false, 36 + "notNull": true, 37 + "autoincrement": false, 38 + "default": "(datetime('now'))" 39 + } 40 + }, 41 + "indexes": {}, 42 + "foreignKeys": {}, 43 + "compositePrimaryKeys": {}, 44 + "uniqueConstraints": {}, 45 + "checkConstraints": {} 46 + }, 47 + "oauth_states": { 48 + "name": "oauth_states", 49 + "columns": { 50 + "key": { 51 + "name": "key", 52 + "type": "text", 53 + "primaryKey": true, 54 + "notNull": true, 55 + "autoincrement": false 56 + }, 57 + "state": { 58 + "name": "state", 59 + "type": "text", 60 + "primaryKey": false, 61 + "notNull": true, 62 + "autoincrement": false 63 + }, 64 + "created_at": { 65 + "name": "created_at", 66 + "type": "text", 67 + "primaryKey": false, 68 + "notNull": true, 69 + "autoincrement": false, 70 + "default": "(datetime('now'))" 71 + } 72 + }, 73 + "indexes": {}, 74 + "foreignKeys": {}, 75 + "compositePrimaryKeys": {}, 76 + "uniqueConstraints": {}, 77 + "checkConstraints": {} 78 + }, 79 + "indexer_cursor": { 80 + "name": "indexer_cursor", 81 + "columns": { 82 + "id": { 83 + "name": "id", 84 + "type": "text", 85 + "primaryKey": true, 86 + "notNull": true, 87 + "autoincrement": false, 88 + "default": "'jetstream'" 89 + }, 90 + "cursor": { 91 + "name": "cursor", 92 + "type": "integer", 93 + "primaryKey": false, 94 + "notNull": true, 95 + "autoincrement": false 96 + }, 97 + "updated_at": { 98 + "name": "updated_at", 99 + "type": "text", 100 + "primaryKey": false, 101 + "notNull": true, 102 + "autoincrement": false, 103 + "default": "(datetime('now'))" 104 + } 105 + }, 106 + "indexes": {}, 107 + "foreignKeys": {}, 108 + "compositePrimaryKeys": {}, 109 + "uniqueConstraints": {}, 110 + "checkConstraints": {} 111 + }, 112 + "sphere_entry_counter": { 113 + "name": "sphere_entry_counter", 114 + "columns": { 115 + "sphere_id": { 116 + "name": "sphere_id", 117 + "type": "text", 118 + "primaryKey": true, 119 + "notNull": true, 120 + "autoincrement": false 121 + }, 122 + "last_number": { 123 + "name": "last_number", 124 + "type": "integer", 125 + "primaryKey": false, 126 + "notNull": true, 127 + "autoincrement": false, 128 + "default": 0 129 + } 130 + }, 131 + "indexes": {}, 132 + "foreignKeys": { 133 + "sphere_entry_counter_sphere_id_spheres_id_fk": { 134 + "name": "sphere_entry_counter_sphere_id_spheres_id_fk", 135 + "tableFrom": "sphere_entry_counter", 136 + "tableTo": "spheres", 137 + "columnsFrom": [ 138 + "sphere_id" 139 + ], 140 + "columnsTo": [ 141 + "id" 142 + ], 143 + "onDelete": "no action", 144 + "onUpdate": "no action" 145 + } 146 + }, 147 + "compositePrimaryKeys": {}, 148 + "uniqueConstraints": {}, 149 + "checkConstraints": {} 150 + }, 151 + "did_handles": { 152 + "name": "did_handles", 153 + "columns": { 154 + "did": { 155 + "name": "did", 156 + "type": "text", 157 + "primaryKey": true, 158 + "notNull": true, 159 + "autoincrement": false 160 + }, 161 + "handle": { 162 + "name": "handle", 163 + "type": "text", 164 + "primaryKey": false, 165 + "notNull": true, 166 + "autoincrement": false 167 + }, 168 + "updated_at": { 169 + "name": "updated_at", 170 + "type": "text", 171 + "primaryKey": false, 172 + "notNull": true, 173 + "autoincrement": false, 174 + "default": "(datetime('now'))" 175 + } 176 + }, 177 + "indexes": {}, 178 + "foreignKeys": {}, 179 + "compositePrimaryKeys": {}, 180 + "uniqueConstraints": {}, 181 + "checkConstraints": {} 182 + }, 183 + "entity_labels": { 184 + "name": "entity_labels", 185 + "columns": { 186 + "label_id": { 187 + "name": "label_id", 188 + "type": "text", 189 + "primaryKey": false, 190 + "notNull": true, 191 + "autoincrement": false 192 + }, 193 + "entity_id": { 194 + "name": "entity_id", 195 + "type": "text", 196 + "primaryKey": false, 197 + "notNull": true, 198 + "autoincrement": false 199 + }, 200 + "entity_type": { 201 + "name": "entity_type", 202 + "type": "text", 203 + "primaryKey": false, 204 + "notNull": true, 205 + "autoincrement": false 206 + } 207 + }, 208 + "indexes": { 209 + "idx_entity_labels_entity": { 210 + "name": "idx_entity_labels_entity", 211 + "columns": [ 212 + "entity_id", 213 + "entity_type" 214 + ], 215 + "isUnique": false 216 + } 217 + }, 218 + "foreignKeys": { 219 + "entity_labels_label_id_sphere_labels_id_fk": { 220 + "name": "entity_labels_label_id_sphere_labels_id_fk", 221 + "tableFrom": "entity_labels", 222 + "tableTo": "sphere_labels", 223 + "columnsFrom": [ 224 + "label_id" 225 + ], 226 + "columnsTo": [ 227 + "id" 228 + ], 229 + "onDelete": "cascade", 230 + "onUpdate": "no action" 231 + } 232 + }, 233 + "compositePrimaryKeys": { 234 + "entity_labels_label_id_entity_id_pk": { 235 + "columns": [ 236 + "label_id", 237 + "entity_id" 238 + ], 239 + "name": "entity_labels_label_id_entity_id_pk" 240 + } 241 + }, 242 + "uniqueConstraints": {}, 243 + "checkConstraints": {} 244 + }, 245 + "label_pds_records": { 246 + "name": "label_pds_records", 247 + "columns": { 248 + "entity_id": { 249 + "name": "entity_id", 250 + "type": "text", 251 + "primaryKey": false, 252 + "notNull": true, 253 + "autoincrement": false 254 + }, 255 + "entity_type": { 256 + "name": "entity_type", 257 + "type": "text", 258 + "primaryKey": false, 259 + "notNull": true, 260 + "autoincrement": false 261 + }, 262 + "actor_did": { 263 + "name": "actor_did", 264 + "type": "text", 265 + "primaryKey": false, 266 + "notNull": true, 267 + "autoincrement": false 268 + }, 269 + "rkey": { 270 + "name": "rkey", 271 + "type": "text", 272 + "primaryKey": false, 273 + "notNull": true, 274 + "autoincrement": false 275 + } 276 + }, 277 + "indexes": {}, 278 + "foreignKeys": {}, 279 + "compositePrimaryKeys": { 280 + "label_pds_records_entity_id_entity_type_actor_did_pk": { 281 + "columns": [ 282 + "entity_id", 283 + "entity_type", 284 + "actor_did" 285 + ], 286 + "name": "label_pds_records_entity_id_entity_type_actor_did_pk" 287 + } 288 + }, 289 + "uniqueConstraints": {}, 290 + "checkConstraints": {} 291 + }, 292 + "sphere_labels": { 293 + "name": "sphere_labels", 294 + "columns": { 295 + "id": { 296 + "name": "id", 297 + "type": "text", 298 + "primaryKey": true, 299 + "notNull": true, 300 + "autoincrement": false 301 + }, 302 + "sphere_id": { 303 + "name": "sphere_id", 304 + "type": "text", 305 + "primaryKey": false, 306 + "notNull": true, 307 + "autoincrement": false 308 + }, 309 + "name": { 310 + "name": "name", 311 + "type": "text", 312 + "primaryKey": false, 313 + "notNull": true, 314 + "autoincrement": false 315 + }, 316 + "description": { 317 + "name": "description", 318 + "type": "text", 319 + "primaryKey": false, 320 + "notNull": false, 321 + "autoincrement": false 322 + }, 323 + "color": { 324 + "name": "color", 325 + "type": "text", 326 + "primaryKey": false, 327 + "notNull": true, 328 + "autoincrement": false 329 + }, 330 + "position": { 331 + "name": "position", 332 + "type": "integer", 333 + "primaryKey": false, 334 + "notNull": true, 335 + "autoincrement": false, 336 + "default": 0 337 + }, 338 + "created_at": { 339 + "name": "created_at", 340 + "type": "text", 341 + "primaryKey": false, 342 + "notNull": true, 343 + "autoincrement": false, 344 + "default": "(datetime('now'))" 345 + } 346 + }, 347 + "indexes": { 348 + "idx_sphere_labels_sphere_name": { 349 + "name": "idx_sphere_labels_sphere_name", 350 + "columns": [ 351 + "sphere_id", 352 + "name" 353 + ], 354 + "isUnique": true 355 + }, 356 + "idx_sphere_labels_sphere_position": { 357 + "name": "idx_sphere_labels_sphere_position", 358 + "columns": [ 359 + "sphere_id", 360 + "position" 361 + ], 362 + "isUnique": false 363 + } 364 + }, 365 + "foreignKeys": { 366 + "sphere_labels_sphere_id_spheres_id_fk": { 367 + "name": "sphere_labels_sphere_id_spheres_id_fk", 368 + "tableFrom": "sphere_labels", 369 + "tableTo": "spheres", 370 + "columnsFrom": [ 371 + "sphere_id" 372 + ], 373 + "columnsTo": [ 374 + "id" 375 + ], 376 + "onDelete": "no action", 377 + "onUpdate": "no action" 378 + } 379 + }, 380 + "compositePrimaryKeys": {}, 381 + "uniqueConstraints": {}, 382 + "checkConstraints": {} 383 + }, 384 + "sphere_members": { 385 + "name": "sphere_members", 386 + "columns": { 387 + "sphere_id": { 388 + "name": "sphere_id", 389 + "type": "text", 390 + "primaryKey": false, 391 + "notNull": true, 392 + "autoincrement": false 393 + }, 394 + "did": { 395 + "name": "did", 396 + "type": "text", 397 + "primaryKey": false, 398 + "notNull": true, 399 + "autoincrement": false 400 + }, 401 + "role": { 402 + "name": "role", 403 + "type": "text", 404 + "primaryKey": false, 405 + "notNull": true, 406 + "autoincrement": false, 407 + "default": "'member'" 408 + }, 409 + "status": { 410 + "name": "status", 411 + "type": "text", 412 + "primaryKey": false, 413 + "notNull": true, 414 + "autoincrement": false, 415 + "default": "'invited'" 416 + }, 417 + "invited_by": { 418 + "name": "invited_by", 419 + "type": "text", 420 + "primaryKey": false, 421 + "notNull": false, 422 + "autoincrement": false 423 + }, 424 + "pds_uri": { 425 + "name": "pds_uri", 426 + "type": "text", 427 + "primaryKey": false, 428 + "notNull": false, 429 + "autoincrement": false 430 + }, 431 + "approval_pds_uri": { 432 + "name": "approval_pds_uri", 433 + "type": "text", 434 + "primaryKey": false, 435 + "notNull": false, 436 + "autoincrement": false 437 + }, 438 + "created_at": { 439 + "name": "created_at", 440 + "type": "text", 441 + "primaryKey": false, 442 + "notNull": true, 443 + "autoincrement": false, 444 + "default": "(datetime('now'))" 445 + } 446 + }, 447 + "indexes": { 448 + "idx_sphere_members_did": { 449 + "name": "idx_sphere_members_did", 450 + "columns": [ 451 + "did" 452 + ], 453 + "isUnique": false 454 + } 455 + }, 456 + "foreignKeys": { 457 + "sphere_members_sphere_id_spheres_id_fk": { 458 + "name": "sphere_members_sphere_id_spheres_id_fk", 459 + "tableFrom": "sphere_members", 460 + "tableTo": "spheres", 461 + "columnsFrom": [ 462 + "sphere_id" 463 + ], 464 + "columnsTo": [ 465 + "id" 466 + ], 467 + "onDelete": "no action", 468 + "onUpdate": "no action" 469 + } 470 + }, 471 + "compositePrimaryKeys": { 472 + "sphere_members_sphere_id_did_pk": { 473 + "columns": [ 474 + "sphere_id", 475 + "did" 476 + ], 477 + "name": "sphere_members_sphere_id_did_pk" 478 + } 479 + }, 480 + "uniqueConstraints": {}, 481 + "checkConstraints": {} 482 + }, 483 + "sphere_modules": { 484 + "name": "sphere_modules", 485 + "columns": { 486 + "sphere_id": { 487 + "name": "sphere_id", 488 + "type": "text", 489 + "primaryKey": false, 490 + "notNull": true, 491 + "autoincrement": false 492 + }, 493 + "module_name": { 494 + "name": "module_name", 495 + "type": "text", 496 + "primaryKey": false, 497 + "notNull": true, 498 + "autoincrement": false 499 + }, 500 + "enabled_at": { 501 + "name": "enabled_at", 502 + "type": "text", 503 + "primaryKey": false, 504 + "notNull": true, 505 + "autoincrement": false, 506 + "default": "(datetime('now'))" 507 + } 508 + }, 509 + "indexes": {}, 510 + "foreignKeys": { 511 + "sphere_modules_sphere_id_spheres_id_fk": { 512 + "name": "sphere_modules_sphere_id_spheres_id_fk", 513 + "tableFrom": "sphere_modules", 514 + "tableTo": "spheres", 515 + "columnsFrom": [ 516 + "sphere_id" 517 + ], 518 + "columnsTo": [ 519 + "id" 520 + ], 521 + "onDelete": "no action", 522 + "onUpdate": "no action" 523 + } 524 + }, 525 + "compositePrimaryKeys": { 526 + "sphere_modules_sphere_id_module_name_pk": { 527 + "columns": [ 528 + "sphere_id", 529 + "module_name" 530 + ], 531 + "name": "sphere_modules_sphere_id_module_name_pk" 532 + } 533 + }, 534 + "uniqueConstraints": {}, 535 + "checkConstraints": {} 536 + }, 537 + "sphere_permissions": { 538 + "name": "sphere_permissions", 539 + "columns": { 540 + "sphere_id": { 541 + "name": "sphere_id", 542 + "type": "text", 543 + "primaryKey": false, 544 + "notNull": true, 545 + "autoincrement": false 546 + }, 547 + "action_key": { 548 + "name": "action_key", 549 + "type": "text", 550 + "primaryKey": false, 551 + "notNull": true, 552 + "autoincrement": false 553 + }, 554 + "min_role": { 555 + "name": "min_role", 556 + "type": "text", 557 + "primaryKey": false, 558 + "notNull": true, 559 + "autoincrement": false 560 + }, 561 + "updated_at": { 562 + "name": "updated_at", 563 + "type": "text", 564 + "primaryKey": false, 565 + "notNull": true, 566 + "autoincrement": false, 567 + "default": "(datetime('now'))" 568 + } 569 + }, 570 + "indexes": {}, 571 + "foreignKeys": { 572 + "sphere_permissions_sphere_id_spheres_id_fk": { 573 + "name": "sphere_permissions_sphere_id_spheres_id_fk", 574 + "tableFrom": "sphere_permissions", 575 + "tableTo": "spheres", 576 + "columnsFrom": [ 577 + "sphere_id" 578 + ], 579 + "columnsTo": [ 580 + "id" 581 + ], 582 + "onDelete": "no action", 583 + "onUpdate": "no action" 584 + } 585 + }, 586 + "compositePrimaryKeys": { 587 + "sphere_permissions_sphere_id_action_key_pk": { 588 + "columns": [ 589 + "sphere_id", 590 + "action_key" 591 + ], 592 + "name": "sphere_permissions_sphere_id_action_key_pk" 593 + } 594 + }, 595 + "uniqueConstraints": {}, 596 + "checkConstraints": {} 597 + }, 598 + "spheres": { 599 + "name": "spheres", 600 + "columns": { 601 + "id": { 602 + "name": "id", 603 + "type": "text", 604 + "primaryKey": true, 605 + "notNull": true, 606 + "autoincrement": false 607 + }, 608 + "handle": { 609 + "name": "handle", 610 + "type": "text", 611 + "primaryKey": false, 612 + "notNull": true, 613 + "autoincrement": false 614 + }, 615 + "name": { 616 + "name": "name", 617 + "type": "text", 618 + "primaryKey": false, 619 + "notNull": true, 620 + "autoincrement": false 621 + }, 622 + "description": { 623 + "name": "description", 624 + "type": "text", 625 + "primaryKey": false, 626 + "notNull": false, 627 + "autoincrement": false 628 + }, 629 + "visibility": { 630 + "name": "visibility", 631 + "type": "text", 632 + "primaryKey": false, 633 + "notNull": true, 634 + "autoincrement": false, 635 + "default": "'public'" 636 + }, 637 + "owner_did": { 638 + "name": "owner_did", 639 + "type": "text", 640 + "primaryKey": false, 641 + "notNull": true, 642 + "autoincrement": false 643 + }, 644 + "pds_uri": { 645 + "name": "pds_uri", 646 + "type": "text", 647 + "primaryKey": false, 648 + "notNull": false, 649 + "autoincrement": false 650 + }, 651 + "created_at": { 652 + "name": "created_at", 653 + "type": "text", 654 + "primaryKey": false, 655 + "notNull": true, 656 + "autoincrement": false, 657 + "default": "(datetime('now'))" 658 + }, 659 + "updated_at": { 660 + "name": "updated_at", 661 + "type": "text", 662 + "primaryKey": false, 663 + "notNull": true, 664 + "autoincrement": false, 665 + "default": "(datetime('now'))" 666 + } 667 + }, 668 + "indexes": { 669 + "spheres_handle_unique": { 670 + "name": "spheres_handle_unique", 671 + "columns": [ 672 + "handle" 673 + ], 674 + "isUnique": true 675 + } 676 + }, 677 + "foreignKeys": {}, 678 + "compositePrimaryKeys": {}, 679 + "uniqueConstraints": {}, 680 + "checkConstraints": {} 681 + }, 682 + "feature_request_comment_votes": { 683 + "name": "feature_request_comment_votes", 684 + "columns": { 685 + "comment_id": { 686 + "name": "comment_id", 687 + "type": "text", 688 + "primaryKey": false, 689 + "notNull": true, 690 + "autoincrement": false 691 + }, 692 + "author_did": { 693 + "name": "author_did", 694 + "type": "text", 695 + "primaryKey": false, 696 + "notNull": true, 697 + "autoincrement": false 698 + }, 699 + "pds_uri": { 700 + "name": "pds_uri", 701 + "type": "text", 702 + "primaryKey": false, 703 + "notNull": false, 704 + "autoincrement": false 705 + }, 706 + "created_at": { 707 + "name": "created_at", 708 + "type": "text", 709 + "primaryKey": false, 710 + "notNull": true, 711 + "autoincrement": false, 712 + "default": "(datetime('now'))" 713 + } 714 + }, 715 + "indexes": { 716 + "idx_feature_request_comment_votes_comment": { 717 + "name": "idx_feature_request_comment_votes_comment", 718 + "columns": [ 719 + "comment_id" 720 + ], 721 + "isUnique": false 722 + } 723 + }, 724 + "foreignKeys": { 725 + "feature_request_comment_votes_comment_id_feature_request_comments_id_fk": { 726 + "name": "feature_request_comment_votes_comment_id_feature_request_comments_id_fk", 727 + "tableFrom": "feature_request_comment_votes", 728 + "tableTo": "feature_request_comments", 729 + "columnsFrom": [ 730 + "comment_id" 731 + ], 732 + "columnsTo": [ 733 + "id" 734 + ], 735 + "onDelete": "no action", 736 + "onUpdate": "no action" 737 + } 738 + }, 739 + "compositePrimaryKeys": { 740 + "feature_request_comment_votes_comment_id_author_did_pk": { 741 + "columns": [ 742 + "comment_id", 743 + "author_did" 744 + ], 745 + "name": "feature_request_comment_votes_comment_id_author_did_pk" 746 + } 747 + }, 748 + "uniqueConstraints": {}, 749 + "checkConstraints": {} 750 + }, 751 + "feature_request_comments": { 752 + "name": "feature_request_comments", 753 + "columns": { 754 + "id": { 755 + "name": "id", 756 + "type": "text", 757 + "primaryKey": true, 758 + "notNull": true, 759 + "autoincrement": false 760 + }, 761 + "request_id": { 762 + "name": "request_id", 763 + "type": "text", 764 + "primaryKey": false, 765 + "notNull": true, 766 + "autoincrement": false 767 + }, 768 + "author_did": { 769 + "name": "author_did", 770 + "type": "text", 771 + "primaryKey": false, 772 + "notNull": true, 773 + "autoincrement": false 774 + }, 775 + "content": { 776 + "name": "content", 777 + "type": "text", 778 + "primaryKey": false, 779 + "notNull": true, 780 + "autoincrement": false 781 + }, 782 + "pds_uri": { 783 + "name": "pds_uri", 784 + "type": "text", 785 + "primaryKey": false, 786 + "notNull": false, 787 + "autoincrement": false 788 + }, 789 + "updated_at": { 790 + "name": "updated_at", 791 + "type": "text", 792 + "primaryKey": false, 793 + "notNull": true, 794 + "autoincrement": false, 795 + "default": "(datetime('now'))" 796 + }, 797 + "hidden_at": { 798 + "name": "hidden_at", 799 + "type": "text", 800 + "primaryKey": false, 801 + "notNull": false, 802 + "autoincrement": false 803 + }, 804 + "moderated_by": { 805 + "name": "moderated_by", 806 + "type": "text", 807 + "primaryKey": false, 808 + "notNull": false, 809 + "autoincrement": false 810 + } 811 + }, 812 + "indexes": { 813 + "idx_feature_request_comments_request": { 814 + "name": "idx_feature_request_comments_request", 815 + "columns": [ 816 + "request_id" 817 + ], 818 + "isUnique": false 819 + }, 820 + "idx_feature_request_comments_author_request": { 821 + "name": "idx_feature_request_comments_author_request", 822 + "columns": [ 823 + "author_did", 824 + "request_id" 825 + ], 826 + "isUnique": false 827 + } 828 + }, 829 + "foreignKeys": { 830 + "feature_request_comments_request_id_feature_requests_id_fk": { 831 + "name": "feature_request_comments_request_id_feature_requests_id_fk", 832 + "tableFrom": "feature_request_comments", 833 + "tableTo": "feature_requests", 834 + "columnsFrom": [ 835 + "request_id" 836 + ], 837 + "columnsTo": [ 838 + "id" 839 + ], 840 + "onDelete": "no action", 841 + "onUpdate": "no action" 842 + } 843 + }, 844 + "compositePrimaryKeys": {}, 845 + "uniqueConstraints": {}, 846 + "checkConstraints": {} 847 + }, 848 + "feature_request_statuses": { 849 + "name": "feature_request_statuses", 850 + "columns": { 851 + "id": { 852 + "name": "id", 853 + "type": "text", 854 + "primaryKey": true, 855 + "notNull": true, 856 + "autoincrement": false 857 + }, 858 + "request_id": { 859 + "name": "request_id", 860 + "type": "text", 861 + "primaryKey": false, 862 + "notNull": true, 863 + "autoincrement": false 864 + }, 865 + "author_did": { 866 + "name": "author_did", 867 + "type": "text", 868 + "primaryKey": false, 869 + "notNull": true, 870 + "autoincrement": false 871 + }, 872 + "status": { 873 + "name": "status", 874 + "type": "text", 875 + "primaryKey": false, 876 + "notNull": true, 877 + "autoincrement": false 878 + }, 879 + "pds_uri": { 880 + "name": "pds_uri", 881 + "type": "text", 882 + "primaryKey": false, 883 + "notNull": false, 884 + "autoincrement": false 885 + } 886 + }, 887 + "indexes": { 888 + "idx_feature_request_statuses_request": { 889 + "name": "idx_feature_request_statuses_request", 890 + "columns": [ 891 + "request_id" 892 + ], 893 + "isUnique": false 894 + } 895 + }, 896 + "foreignKeys": { 897 + "feature_request_statuses_request_id_feature_requests_id_fk": { 898 + "name": "feature_request_statuses_request_id_feature_requests_id_fk", 899 + "tableFrom": "feature_request_statuses", 900 + "tableTo": "feature_requests", 901 + "columnsFrom": [ 902 + "request_id" 903 + ], 904 + "columnsTo": [ 905 + "id" 906 + ], 907 + "onDelete": "no action", 908 + "onUpdate": "no action" 909 + } 910 + }, 911 + "compositePrimaryKeys": {}, 912 + "uniqueConstraints": {}, 913 + "checkConstraints": {} 914 + }, 915 + "feature_request_votes": { 916 + "name": "feature_request_votes", 917 + "columns": { 918 + "request_id": { 919 + "name": "request_id", 920 + "type": "text", 921 + "primaryKey": false, 922 + "notNull": true, 923 + "autoincrement": false 924 + }, 925 + "author_did": { 926 + "name": "author_did", 927 + "type": "text", 928 + "primaryKey": false, 929 + "notNull": true, 930 + "autoincrement": false 931 + }, 932 + "pds_uri": { 933 + "name": "pds_uri", 934 + "type": "text", 935 + "primaryKey": false, 936 + "notNull": false, 937 + "autoincrement": false 938 + }, 939 + "created_at": { 940 + "name": "created_at", 941 + "type": "text", 942 + "primaryKey": false, 943 + "notNull": true, 944 + "autoincrement": false, 945 + "default": "(datetime('now'))" 946 + } 947 + }, 948 + "indexes": { 949 + "idx_feature_request_votes_request": { 950 + "name": "idx_feature_request_votes_request", 951 + "columns": [ 952 + "request_id" 953 + ], 954 + "isUnique": false 955 + } 956 + }, 957 + "foreignKeys": { 958 + "feature_request_votes_request_id_feature_requests_id_fk": { 959 + "name": "feature_request_votes_request_id_feature_requests_id_fk", 960 + "tableFrom": "feature_request_votes", 961 + "tableTo": "feature_requests", 962 + "columnsFrom": [ 963 + "request_id" 964 + ], 965 + "columnsTo": [ 966 + "id" 967 + ], 968 + "onDelete": "no action", 969 + "onUpdate": "no action" 970 + } 971 + }, 972 + "compositePrimaryKeys": { 973 + "feature_request_votes_request_id_author_did_pk": { 974 + "columns": [ 975 + "request_id", 976 + "author_did" 977 + ], 978 + "name": "feature_request_votes_request_id_author_did_pk" 979 + } 980 + }, 981 + "uniqueConstraints": {}, 982 + "checkConstraints": {} 983 + }, 984 + "feature_requests": { 985 + "name": "feature_requests", 986 + "columns": { 987 + "id": { 988 + "name": "id", 989 + "type": "text", 990 + "primaryKey": true, 991 + "notNull": true, 992 + "autoincrement": false 993 + }, 994 + "sphere_id": { 995 + "name": "sphere_id", 996 + "type": "text", 997 + "primaryKey": false, 998 + "notNull": true, 999 + "autoincrement": false 1000 + }, 1001 + "number": { 1002 + "name": "number", 1003 + "type": "integer", 1004 + "primaryKey": false, 1005 + "notNull": true, 1006 + "autoincrement": false 1007 + }, 1008 + "author_did": { 1009 + "name": "author_did", 1010 + "type": "text", 1011 + "primaryKey": false, 1012 + "notNull": true, 1013 + "autoincrement": false 1014 + }, 1015 + "title": { 1016 + "name": "title", 1017 + "type": "text", 1018 + "primaryKey": false, 1019 + "notNull": true, 1020 + "autoincrement": false 1021 + }, 1022 + "description": { 1023 + "name": "description", 1024 + "type": "text", 1025 + "primaryKey": false, 1026 + "notNull": true, 1027 + "autoincrement": false 1028 + }, 1029 + "status": { 1030 + "name": "status", 1031 + "type": "text", 1032 + "primaryKey": false, 1033 + "notNull": true, 1034 + "autoincrement": false, 1035 + "default": "'requested'" 1036 + }, 1037 + "duplicate_of_id": { 1038 + "name": "duplicate_of_id", 1039 + "type": "text", 1040 + "primaryKey": false, 1041 + "notNull": false, 1042 + "autoincrement": false 1043 + }, 1044 + "pds_uri": { 1045 + "name": "pds_uri", 1046 + "type": "text", 1047 + "primaryKey": false, 1048 + "notNull": false, 1049 + "autoincrement": false 1050 + }, 1051 + "hidden_at": { 1052 + "name": "hidden_at", 1053 + "type": "text", 1054 + "primaryKey": false, 1055 + "notNull": false, 1056 + "autoincrement": false 1057 + }, 1058 + "moderated_by": { 1059 + "name": "moderated_by", 1060 + "type": "text", 1061 + "primaryKey": false, 1062 + "notNull": false, 1063 + "autoincrement": false 1064 + }, 1065 + "label_updated_at": { 1066 + "name": "label_updated_at", 1067 + "type": "text", 1068 + "primaryKey": false, 1069 + "notNull": false, 1070 + "autoincrement": false 1071 + }, 1072 + "updated_at": { 1073 + "name": "updated_at", 1074 + "type": "text", 1075 + "primaryKey": false, 1076 + "notNull": true, 1077 + "autoincrement": false, 1078 + "default": "(datetime('now'))" 1079 + } 1080 + }, 1081 + "indexes": { 1082 + "idx_feature_requests_sphere_number": { 1083 + "name": "idx_feature_requests_sphere_number", 1084 + "columns": [ 1085 + "sphere_id", 1086 + "number" 1087 + ], 1088 + "isUnique": true 1089 + }, 1090 + "idx_feature_requests_sphere_hidden_status": { 1091 + "name": "idx_feature_requests_sphere_hidden_status", 1092 + "columns": [ 1093 + "sphere_id", 1094 + "hidden_at", 1095 + "status" 1096 + ], 1097 + "isUnique": false 1098 + } 1099 + }, 1100 + "foreignKeys": { 1101 + "feature_requests_sphere_id_spheres_id_fk": { 1102 + "name": "feature_requests_sphere_id_spheres_id_fk", 1103 + "tableFrom": "feature_requests", 1104 + "tableTo": "spheres", 1105 + "columnsFrom": [ 1106 + "sphere_id" 1107 + ], 1108 + "columnsTo": [ 1109 + "id" 1110 + ], 1111 + "onDelete": "no action", 1112 + "onUpdate": "no action" 1113 + } 1114 + }, 1115 + "compositePrimaryKeys": {}, 1116 + "uniqueConstraints": {}, 1117 + "checkConstraints": {} 1118 + }, 1119 + "feed_posts": { 1120 + "name": "feed_posts", 1121 + "columns": { 1122 + "id": { 1123 + "name": "id", 1124 + "type": "text", 1125 + "primaryKey": true, 1126 + "notNull": true, 1127 + "autoincrement": false 1128 + }, 1129 + "author_did": { 1130 + "name": "author_did", 1131 + "type": "text", 1132 + "primaryKey": false, 1133 + "notNull": true, 1134 + "autoincrement": false 1135 + }, 1136 + "content": { 1137 + "name": "content", 1138 + "type": "text", 1139 + "primaryKey": false, 1140 + "notNull": true, 1141 + "autoincrement": false 1142 + }, 1143 + "parent_id": { 1144 + "name": "parent_id", 1145 + "type": "text", 1146 + "primaryKey": false, 1147 + "notNull": false, 1148 + "autoincrement": false 1149 + }, 1150 + "pds_uri": { 1151 + "name": "pds_uri", 1152 + "type": "text", 1153 + "primaryKey": false, 1154 + "notNull": false, 1155 + "autoincrement": false 1156 + }, 1157 + "updated_at": { 1158 + "name": "updated_at", 1159 + "type": "text", 1160 + "primaryKey": false, 1161 + "notNull": true, 1162 + "autoincrement": false, 1163 + "default": "(datetime('now'))" 1164 + } 1165 + }, 1166 + "indexes": { 1167 + "idx_feed_posts_parent": { 1168 + "name": "idx_feed_posts_parent", 1169 + "columns": [ 1170 + "parent_id" 1171 + ], 1172 + "isUnique": false 1173 + } 1174 + }, 1175 + "foreignKeys": {}, 1176 + "compositePrimaryKeys": {}, 1177 + "uniqueConstraints": {}, 1178 + "checkConstraints": {} 1179 + }, 1180 + "kanban_columns": { 1181 + "name": "kanban_columns", 1182 + "columns": { 1183 + "id": { 1184 + "name": "id", 1185 + "type": "text", 1186 + "primaryKey": true, 1187 + "notNull": true, 1188 + "autoincrement": false 1189 + }, 1190 + "sphere_id": { 1191 + "name": "sphere_id", 1192 + "type": "text", 1193 + "primaryKey": false, 1194 + "notNull": true, 1195 + "autoincrement": false 1196 + }, 1197 + "slug": { 1198 + "name": "slug", 1199 + "type": "text", 1200 + "primaryKey": false, 1201 + "notNull": true, 1202 + "autoincrement": false 1203 + }, 1204 + "label": { 1205 + "name": "label", 1206 + "type": "text", 1207 + "primaryKey": false, 1208 + "notNull": true, 1209 + "autoincrement": false 1210 + }, 1211 + "status_type": { 1212 + "name": "status_type", 1213 + "type": "text", 1214 + "primaryKey": false, 1215 + "notNull": true, 1216 + "autoincrement": false, 1217 + "default": "'backlog'" 1218 + }, 1219 + "position": { 1220 + "name": "position", 1221 + "type": "integer", 1222 + "primaryKey": false, 1223 + "notNull": true, 1224 + "autoincrement": false 1225 + }, 1226 + "created_at": { 1227 + "name": "created_at", 1228 + "type": "text", 1229 + "primaryKey": false, 1230 + "notNull": true, 1231 + "autoincrement": false, 1232 + "default": "(datetime('now'))" 1233 + } 1234 + }, 1235 + "indexes": { 1236 + "idx_kanban_columns_sphere_slug": { 1237 + "name": "idx_kanban_columns_sphere_slug", 1238 + "columns": [ 1239 + "sphere_id", 1240 + "slug" 1241 + ], 1242 + "isUnique": true 1243 + }, 1244 + "idx_kanban_columns_sphere_position": { 1245 + "name": "idx_kanban_columns_sphere_position", 1246 + "columns": [ 1247 + "sphere_id", 1248 + "position" 1249 + ], 1250 + "isUnique": false 1251 + } 1252 + }, 1253 + "foreignKeys": { 1254 + "kanban_columns_sphere_id_spheres_id_fk": { 1255 + "name": "kanban_columns_sphere_id_spheres_id_fk", 1256 + "tableFrom": "kanban_columns", 1257 + "tableTo": "spheres", 1258 + "columnsFrom": [ 1259 + "sphere_id" 1260 + ], 1261 + "columnsTo": [ 1262 + "id" 1263 + ], 1264 + "onDelete": "no action", 1265 + "onUpdate": "no action" 1266 + } 1267 + }, 1268 + "compositePrimaryKeys": {}, 1269 + "uniqueConstraints": {}, 1270 + "checkConstraints": {} 1271 + }, 1272 + "kanban_task_comments": { 1273 + "name": "kanban_task_comments", 1274 + "columns": { 1275 + "id": { 1276 + "name": "id", 1277 + "type": "text", 1278 + "primaryKey": true, 1279 + "notNull": true, 1280 + "autoincrement": false 1281 + }, 1282 + "task_id": { 1283 + "name": "task_id", 1284 + "type": "text", 1285 + "primaryKey": false, 1286 + "notNull": true, 1287 + "autoincrement": false 1288 + }, 1289 + "author_did": { 1290 + "name": "author_did", 1291 + "type": "text", 1292 + "primaryKey": false, 1293 + "notNull": true, 1294 + "autoincrement": false 1295 + }, 1296 + "content": { 1297 + "name": "content", 1298 + "type": "text", 1299 + "primaryKey": false, 1300 + "notNull": true, 1301 + "autoincrement": false 1302 + }, 1303 + "pds_uri": { 1304 + "name": "pds_uri", 1305 + "type": "text", 1306 + "primaryKey": false, 1307 + "notNull": false, 1308 + "autoincrement": false 1309 + }, 1310 + "updated_at": { 1311 + "name": "updated_at", 1312 + "type": "text", 1313 + "primaryKey": false, 1314 + "notNull": true, 1315 + "autoincrement": false, 1316 + "default": "(datetime('now'))" 1317 + }, 1318 + "hidden_at": { 1319 + "name": "hidden_at", 1320 + "type": "text", 1321 + "primaryKey": false, 1322 + "notNull": false, 1323 + "autoincrement": false 1324 + }, 1325 + "moderated_by": { 1326 + "name": "moderated_by", 1327 + "type": "text", 1328 + "primaryKey": false, 1329 + "notNull": false, 1330 + "autoincrement": false 1331 + } 1332 + }, 1333 + "indexes": { 1334 + "idx_kanban_task_comments_task": { 1335 + "name": "idx_kanban_task_comments_task", 1336 + "columns": [ 1337 + "task_id" 1338 + ], 1339 + "isUnique": false 1340 + }, 1341 + "idx_kanban_task_comments_author_task": { 1342 + "name": "idx_kanban_task_comments_author_task", 1343 + "columns": [ 1344 + "author_did", 1345 + "task_id" 1346 + ], 1347 + "isUnique": false 1348 + } 1349 + }, 1350 + "foreignKeys": { 1351 + "kanban_task_comments_task_id_kanban_tasks_id_fk": { 1352 + "name": "kanban_task_comments_task_id_kanban_tasks_id_fk", 1353 + "tableFrom": "kanban_task_comments", 1354 + "tableTo": "kanban_tasks", 1355 + "columnsFrom": [ 1356 + "task_id" 1357 + ], 1358 + "columnsTo": [ 1359 + "id" 1360 + ], 1361 + "onDelete": "no action", 1362 + "onUpdate": "no action" 1363 + } 1364 + }, 1365 + "compositePrimaryKeys": {}, 1366 + "uniqueConstraints": {}, 1367 + "checkConstraints": {} 1368 + }, 1369 + "kanban_task_status_changes": { 1370 + "name": "kanban_task_status_changes", 1371 + "columns": { 1372 + "id": { 1373 + "name": "id", 1374 + "type": "text", 1375 + "primaryKey": true, 1376 + "notNull": true, 1377 + "autoincrement": false 1378 + }, 1379 + "task_id": { 1380 + "name": "task_id", 1381 + "type": "text", 1382 + "primaryKey": false, 1383 + "notNull": true, 1384 + "autoincrement": false 1385 + }, 1386 + "author_did": { 1387 + "name": "author_did", 1388 + "type": "text", 1389 + "primaryKey": false, 1390 + "notNull": true, 1391 + "autoincrement": false 1392 + }, 1393 + "status": { 1394 + "name": "status", 1395 + "type": "text", 1396 + "primaryKey": false, 1397 + "notNull": true, 1398 + "autoincrement": false 1399 + }, 1400 + "pds_uri": { 1401 + "name": "pds_uri", 1402 + "type": "text", 1403 + "primaryKey": false, 1404 + "notNull": false, 1405 + "autoincrement": false 1406 + } 1407 + }, 1408 + "indexes": { 1409 + "idx_kanban_task_status_changes_task": { 1410 + "name": "idx_kanban_task_status_changes_task", 1411 + "columns": [ 1412 + "task_id" 1413 + ], 1414 + "isUnique": false 1415 + } 1416 + }, 1417 + "foreignKeys": { 1418 + "kanban_task_status_changes_task_id_kanban_tasks_id_fk": { 1419 + "name": "kanban_task_status_changes_task_id_kanban_tasks_id_fk", 1420 + "tableFrom": "kanban_task_status_changes", 1421 + "tableTo": "kanban_tasks", 1422 + "columnsFrom": [ 1423 + "task_id" 1424 + ], 1425 + "columnsTo": [ 1426 + "id" 1427 + ], 1428 + "onDelete": "no action", 1429 + "onUpdate": "no action" 1430 + } 1431 + }, 1432 + "compositePrimaryKeys": {}, 1433 + "uniqueConstraints": {}, 1434 + "checkConstraints": {} 1435 + }, 1436 + "kanban_tasks": { 1437 + "name": "kanban_tasks", 1438 + "columns": { 1439 + "id": { 1440 + "name": "id", 1441 + "type": "text", 1442 + "primaryKey": true, 1443 + "notNull": true, 1444 + "autoincrement": false 1445 + }, 1446 + "sphere_id": { 1447 + "name": "sphere_id", 1448 + "type": "text", 1449 + "primaryKey": false, 1450 + "notNull": true, 1451 + "autoincrement": false 1452 + }, 1453 + "number": { 1454 + "name": "number", 1455 + "type": "integer", 1456 + "primaryKey": false, 1457 + "notNull": true, 1458 + "autoincrement": false 1459 + }, 1460 + "author_did": { 1461 + "name": "author_did", 1462 + "type": "text", 1463 + "primaryKey": false, 1464 + "notNull": true, 1465 + "autoincrement": false 1466 + }, 1467 + "title": { 1468 + "name": "title", 1469 + "type": "text", 1470 + "primaryKey": false, 1471 + "notNull": true, 1472 + "autoincrement": false 1473 + }, 1474 + "description": { 1475 + "name": "description", 1476 + "type": "text", 1477 + "primaryKey": false, 1478 + "notNull": true, 1479 + "autoincrement": false, 1480 + "default": "''" 1481 + }, 1482 + "status": { 1483 + "name": "status", 1484 + "type": "text", 1485 + "primaryKey": false, 1486 + "notNull": true, 1487 + "autoincrement": false, 1488 + "default": "'backlog'" 1489 + }, 1490 + "position": { 1491 + "name": "position", 1492 + "type": "integer", 1493 + "primaryKey": false, 1494 + "notNull": true, 1495 + "autoincrement": false, 1496 + "default": 0 1497 + }, 1498 + "assignee_did": { 1499 + "name": "assignee_did", 1500 + "type": "text", 1501 + "primaryKey": false, 1502 + "notNull": false, 1503 + "autoincrement": false 1504 + }, 1505 + "pds_uri": { 1506 + "name": "pds_uri", 1507 + "type": "text", 1508 + "primaryKey": false, 1509 + "notNull": false, 1510 + "autoincrement": false 1511 + }, 1512 + "hidden_at": { 1513 + "name": "hidden_at", 1514 + "type": "text", 1515 + "primaryKey": false, 1516 + "notNull": false, 1517 + "autoincrement": false 1518 + }, 1519 + "moderated_by": { 1520 + "name": "moderated_by", 1521 + "type": "text", 1522 + "primaryKey": false, 1523 + "notNull": false, 1524 + "autoincrement": false 1525 + }, 1526 + "label_updated_at": { 1527 + "name": "label_updated_at", 1528 + "type": "text", 1529 + "primaryKey": false, 1530 + "notNull": false, 1531 + "autoincrement": false 1532 + }, 1533 + "updated_at": { 1534 + "name": "updated_at", 1535 + "type": "text", 1536 + "primaryKey": false, 1537 + "notNull": true, 1538 + "autoincrement": false, 1539 + "default": "(datetime('now'))" 1540 + } 1541 + }, 1542 + "indexes": { 1543 + "idx_kanban_tasks_sphere_number": { 1544 + "name": "idx_kanban_tasks_sphere_number", 1545 + "columns": [ 1546 + "sphere_id", 1547 + "number" 1548 + ], 1549 + "isUnique": true 1550 + }, 1551 + "idx_kanban_tasks_sphere_status_position": { 1552 + "name": "idx_kanban_tasks_sphere_status_position", 1553 + "columns": [ 1554 + "sphere_id", 1555 + "status", 1556 + "position" 1557 + ], 1558 + "isUnique": false 1559 + }, 1560 + "idx_kanban_tasks_sphere_hidden": { 1561 + "name": "idx_kanban_tasks_sphere_hidden", 1562 + "columns": [ 1563 + "sphere_id", 1564 + "hidden_at" 1565 + ], 1566 + "isUnique": false 1567 + } 1568 + }, 1569 + "foreignKeys": { 1570 + "kanban_tasks_sphere_id_spheres_id_fk": { 1571 + "name": "kanban_tasks_sphere_id_spheres_id_fk", 1572 + "tableFrom": "kanban_tasks", 1573 + "tableTo": "spheres", 1574 + "columnsFrom": [ 1575 + "sphere_id" 1576 + ], 1577 + "columnsTo": [ 1578 + "id" 1579 + ], 1580 + "onDelete": "no action", 1581 + "onUpdate": "no action" 1582 + } 1583 + }, 1584 + "compositePrimaryKeys": {}, 1585 + "uniqueConstraints": {}, 1586 + "checkConstraints": {} 1587 + } 1588 + }, 1589 + "views": {}, 1590 + "enums": {}, 1591 + "_meta": { 1592 + "schemas": {}, 1593 + "tables": {}, 1594 + "columns": {} 1595 + }, 1596 + "internal": { 1597 + "indexes": {} 1598 + } 1599 + }
+10 -3
drizzle/meta/_journal.json
··· 68 68 { 69 69 "idx": 9, 70 70 "version": "6", 71 - "when": 1776516974088, 71 + "when": 1776800000001, 72 72 "tag": "0009_damp_microbe", 73 73 "breakpoints": true 74 74 }, 75 75 { 76 76 "idx": 10, 77 77 "version": "6", 78 - "when": 1776547678213, 78 + "when": 1776800000002, 79 79 "tag": "0010_dark_red_skull", 80 80 "breakpoints": true 81 + }, 82 + { 83 + "idx": 11, 84 + "version": "6", 85 + "when": 1776800000003, 86 + "tag": "0011_military_diamondback", 87 + "breakpoints": true 81 88 } 82 89 ] 83 - } 90 + }
+1 -1
package.json
··· 28 28 "pds:logs": "docker compose -f docker-compose.dev.yml logs -f pds", 29 29 "pds:account": "bun run scripts/pds-account.ts", 30 30 "generate:lexicons": "bun scripts/generate-lexicon-types.ts", 31 - "db:generate": "drizzle-kit generate", 31 + "db:generate": "drizzle-kit generate && bun run scripts/fix-journal-ordering.ts", 32 32 "db:migrate": "bun run packages/core/src/db/migrate.ts", 33 33 "build": "vp build --outDir dist/client && vp build --ssr src/entry-server.tsx --outDir dist/server --emptyOutDir false", 34 34 "start": "NODE_ENV=production bun run packages/app/src/server.ts",
+15 -5
packages/app/src/client.tsx
··· 41 41 ssrPageData.value = ssrData.pageData; 42 42 } 43 43 } else { 44 - // Fallback: no SSR data (dev without SSR), behave as before 44 + // Fallback: no SSR data (dev without SSR), behave as before. Note: this 45 + // branch has no `pageshow` listener, so bfcache restores keep stale data. 46 + // Acceptable because this path isn't hit in prod. 45 47 checkSession(); 46 48 const handle = getSphereHandleFromUrl(); 47 49 loadSphere(handle ?? undefined); ··· 49 51 50 52 hydrate(<App />, document.getElementById("app")!); 51 53 52 - // After hydration, silently refresh data in background to catch changes 53 - // Use refreshSphere (not loadSphere) to avoid resetting state to pending/null 54 + // SSR already seeded auth + sphere from the server's own cache, so the page 55 + // ships hydrated with fresh data. No immediate refresh — that was just a 56 + // duplicate round-trip. Instead, refresh on `pageshow` when the page comes 57 + // out of the bfcache (back/forward nav) where SSR data can be arbitrarily 58 + // stale. Regular navigations call loadSphere/refreshSphere explicitly where 59 + // the UX expects it (settings edits, invitation banner). 54 60 if (ssrData) { 55 - checkSession(); 56 - refreshSphere(); 61 + window.addEventListener("pageshow", (event) => { 62 + if (event.persisted) { 63 + checkSession(); 64 + refreshSphere(); 65 + } 66 + }); 57 67 }
+33
packages/core/src/db/migrate.ts
··· 1 1 import { Database } from "bun:sqlite"; 2 + import { readFileSync } from "node:fs"; 3 + import { join } from "node:path"; 2 4 import { migrate } from "drizzle-orm/bun-sqlite/migrator"; 3 5 import { getDb } from "./index.ts"; 4 6 5 7 const migrationsFolder = process.env.MIGRATIONS_PATH || "drizzle"; 6 8 9 + assertJournalMonotonic(migrationsFolder); 7 10 migrate(getDb(), { migrationsFolder }); 11 + 12 + /** 13 + * The sqlite migrator skips any migration whose `when` is ≤ 14 + * `max(__drizzle_migrations.created_at)` on the target DB. If the journal has 15 + * non-monotonic `when` values, a newer entry can silently get skipped — the 16 + * kind of bug that only surfaces as "that table is missing in prod". Fail 17 + * loudly at startup instead, pointing to the fix script. 18 + */ 19 + function assertJournalMonotonic(folder: string): void { 20 + const journalPath = join(folder, "meta/_journal.json"); 21 + let journal: { entries: { idx: number; when: number; tag: string }[] }; 22 + try { 23 + journal = JSON.parse(readFileSync(journalPath, "utf-8")); 24 + } catch { 25 + // No journal (fresh project) — nothing to validate. 26 + return; 27 + } 28 + const entries = [...journal.entries].sort((a, b) => a.idx - b.idx); 29 + let maxWhen = Number.NEGATIVE_INFINITY; 30 + for (const entry of entries) { 31 + if (entry.when <= maxWhen) { 32 + throw new Error( 33 + `[migrate] journal is not monotonic: ${entry.tag} has when=${entry.when} but a prior ` + 34 + `entry has when=${maxWhen}. The migrator would silently skip this migration. ` + 35 + `Run \`bun run scripts/fix-journal-ordering.ts\` to repair the journal, then commit.`, 36 + ); 37 + } 38 + maxWhen = entry.when; 39 + } 40 + } 8 41 9 42 // Data migration: renumber feature requests and kanban tasks to use the 10 43 // shared sphere_entry_counter. Runs only once — skips if counter rows exist.
+10 -2
packages/feature-requests/src/db/schema.ts
··· 36 36 }, 37 37 (table) => [ 38 38 uniqueIndex("idx_feature_requests_sphere_number").on(table.sphereId, table.number), 39 - index("idx_feature_requests_sphere").on(table.sphereId), 40 - index("idx_feature_requests_status").on(table.status), 39 + // Covers the dashboard stats query and the status-filtered list query — 40 + // both filter on sphere + visibility (+ optional status), matching this 41 + // composite's column order exactly, so SQLite can satisfy them from the 42 + // index alone. Sphere-only filters use the leftmost prefix. Replaces the 43 + // prior single-column (sphere_id) and (status) indexes. 44 + index("idx_feature_requests_sphere_hidden_status").on( 45 + table.sphereId, 46 + table.hiddenAt, 47 + table.status, 48 + ), 41 49 ], 42 50 ); 43 51
+230
packages/kanban/src/__tests__/stats.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach } from "vitest"; 2 + import { Hono } from "hono"; 3 + import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3"; 4 + 5 + import { createTestDb, seedSphere } from "../../../core/src/__tests__/helpers/test-db.ts"; 6 + 7 + let db: BetterSQLite3Database; 8 + 9 + vi.mock("@exosphere/core/db", () => ({ 10 + getDb: () => db, 11 + })); 12 + 13 + import { kanbanTasks, kanbanColumns } from "../db/schema.ts"; 14 + import { statsApi } from "../api/stats.ts"; 15 + 16 + const SPHERE_ID = "test-sphere-001"; 17 + const OWNER_DID = "did:plc:owner"; 18 + const AUTHOR_DID = "did:plc:author"; 19 + 20 + interface TaskSeed { 21 + id: string; 22 + number: number; 23 + status: string; 24 + title?: string; 25 + } 26 + 27 + function seedColumn( 28 + slug: string, 29 + statusType: string, 30 + opts: { label?: string; position?: number } = {}, 31 + ) { 32 + db.insert(kanbanColumns) 33 + .values({ 34 + id: `col-${slug}`, 35 + sphereId: SPHERE_ID, 36 + slug, 37 + label: opts.label ?? slug, 38 + statusType, 39 + position: opts.position ?? 0, 40 + }) 41 + .run(); 42 + } 43 + 44 + function seedTask(t: TaskSeed) { 45 + db.insert(kanbanTasks) 46 + .values({ 47 + id: t.id, 48 + sphereId: SPHERE_ID, 49 + number: t.number, 50 + authorDid: AUTHOR_DID, 51 + title: t.title ?? `Task ${t.number}`, 52 + status: t.status, 53 + position: t.number * 1000, 54 + }) 55 + .run(); 56 + } 57 + 58 + /** Mini app: inject sphereId via middleware, mount the real statsApi. */ 59 + function buildApp() { 60 + const app = new Hono(); 61 + app.use("*", async (c, next) => { 62 + c.set("sphereId", SPHERE_ID); 63 + await next(); 64 + }); 65 + app.route("/", statsApi); 66 + return app; 67 + } 68 + 69 + beforeEach(() => { 70 + db = createTestDb(); 71 + seedSphere(db, { id: SPHERE_ID, handle: "test.bsky.social", ownerDid: OWNER_DID }); 72 + }); 73 + 74 + describe("GET /stats", () => { 75 + it("counts tasks by statusType via the column join", async () => { 76 + seedColumn("backlog", "backlog"); 77 + seedColumn("doing", "started", { label: "Doing" }); 78 + seedColumn("done", "completed", { label: "Done" }); 79 + 80 + seedTask({ id: "3mhy7w6tbg22a", number: 1, status: "backlog" }); 81 + seedTask({ id: "3mhy7w6tbg22b", number: 2, status: "backlog" }); 82 + seedTask({ id: "3mhy7w6tbg22c", number: 3, status: "doing" }); 83 + seedTask({ id: "3mhy7w6tbg22d", number: 4, status: "done" }); 84 + 85 + const res = await buildApp().request("/stats"); 86 + expect(res.status).toBe(200); 87 + const body = (await res.json()) as { 88 + total: number; 89 + statusTypeCounts: Record<string, number>; 90 + }; 91 + 92 + expect(body.total).toBe(4); 93 + expect(body.statusTypeCounts.backlog).toBe(2); 94 + expect(body.statusTypeCounts.started).toBe(1); 95 + expect(body.statusTypeCounts.completed).toBe(1); 96 + expect(body.statusTypeCounts.planned).toBe(0); 97 + expect(body.statusTypeCounts.canceled).toBe(0); 98 + }); 99 + 100 + it("still counts orphan tasks toward total when their status slug has no matching column", async () => { 101 + // Only one column mapped; the "removed-status" slug has no column, 102 + // simulating a column that was deleted while tasks still reference it. 103 + seedColumn("backlog", "backlog"); 104 + 105 + seedTask({ id: "3mhy7w6tbg22a", number: 1, status: "backlog" }); 106 + seedTask({ id: "3mhy7w6tbg22b", number: 2, status: "removed-status" }); 107 + seedTask({ id: "3mhy7w6tbg22c", number: 3, status: "also-gone" }); 108 + 109 + const res = await buildApp().request("/stats"); 110 + expect(res.status).toBe(200); 111 + const body = (await res.json()) as { 112 + total: number; 113 + statusTypeCounts: Record<string, number>; 114 + }; 115 + 116 + // All three tasks are counted — the orphans must not silently drop. 117 + expect(body.total).toBe(3); 118 + // Only the mapped task contributes to a named statusType bucket. 119 + expect(body.statusTypeCounts.backlog).toBe(1); 120 + expect(body.statusTypeCounts.started).toBe(0); 121 + expect(body.statusTypeCounts.completed).toBe(0); 122 + }); 123 + 124 + it("isolates counts per sphere — tasks from other spheres never leak in", async () => { 125 + const OTHER_SPHERE = "test-sphere-002"; 126 + seedSphere(db, { id: OTHER_SPHERE, handle: "other.bsky.social", ownerDid: OWNER_DID }); 127 + 128 + // Current sphere: 1 task. 129 + seedColumn("backlog", "backlog"); 130 + seedTask({ id: "3mhy7w6tbg22a", number: 1, status: "backlog" }); 131 + 132 + // Other sphere: 3 tasks, its own column. These must not show up. 133 + db.insert(kanbanColumns) 134 + .values({ 135 + id: "col-other-backlog", 136 + sphereId: OTHER_SPHERE, 137 + slug: "backlog", 138 + label: "backlog", 139 + statusType: "backlog", 140 + position: 0, 141 + }) 142 + .run(); 143 + for (let i = 0; i < 3; i++) { 144 + db.insert(kanbanTasks) 145 + .values({ 146 + id: `3mhy7w6tbg23${String.fromCharCode(97 + i)}`, 147 + sphereId: OTHER_SPHERE, 148 + number: i + 1, 149 + authorDid: AUTHOR_DID, 150 + title: `Other ${i}`, 151 + status: "backlog", 152 + position: (i + 1) * 1000, 153 + }) 154 + .run(); 155 + } 156 + 157 + const res = await buildApp().request("/stats"); 158 + const body = (await res.json()) as { 159 + total: number; 160 + statusTypeCounts: Record<string, number>; 161 + latestTasks: unknown[]; 162 + }; 163 + 164 + expect(body.total).toBe(1); 165 + expect(body.statusTypeCounts.backlog).toBe(1); 166 + expect(body.latestTasks).toHaveLength(1); 167 + }); 168 + 169 + it("returns latestTasks enriched with statusType + label, excluding orphans", async () => { 170 + seedColumn("backlog", "backlog", { label: "Backlog" }); 171 + seedColumn("doing", "started", { label: "Doing" }); 172 + 173 + // Newest-first by TID: 22d > 22c > 22b > 22a 174 + seedTask({ id: "3mhy7w6tbg22a", number: 1, status: "backlog", title: "oldest" }); 175 + seedTask({ id: "3mhy7w6tbg22b", number: 2, status: "doing", title: "second" }); 176 + // Orphan at position 3 — should be filtered from latestTasks but still counted. 177 + seedTask({ id: "3mhy7w6tbg22c", number: 3, status: "vanished", title: "orphan" }); 178 + seedTask({ id: "3mhy7w6tbg22d", number: 4, status: "backlog", title: "newest" }); 179 + 180 + const res = await buildApp().request("/stats"); 181 + const body = (await res.json()) as { 182 + total: number; 183 + statusTypeCounts: Record<string, number>; 184 + latestTasks: { 185 + id: string; 186 + title: string; 187 + statusType: string; 188 + statusLabel: string; 189 + }[]; 190 + }; 191 + 192 + expect(body.total).toBe(4); 193 + // Orphan absent from the list even though it's newest-but-one. 194 + const titles = body.latestTasks.map((t) => t.title); 195 + expect(titles).not.toContain("orphan"); 196 + expect(titles[0]).toBe("newest"); 197 + // Shape check: statusType and label come from the joined column row. 198 + const newest = body.latestTasks[0]; 199 + expect(newest.statusType).toBe("backlog"); 200 + expect(newest.statusLabel).toBe("Backlog"); 201 + }); 202 + 203 + it("excludes hidden tasks from total and per-status counts", async () => { 204 + seedColumn("backlog", "backlog"); 205 + 206 + seedTask({ id: "3mhy7w6tbg22a", number: 1, status: "backlog" }); 207 + // Hidden task — should not contribute. 208 + db.insert(kanbanTasks) 209 + .values({ 210 + id: "3mhy7w6tbg22b", 211 + sphereId: SPHERE_ID, 212 + number: 2, 213 + authorDid: AUTHOR_DID, 214 + title: "Hidden", 215 + status: "backlog", 216 + position: 2000, 217 + hiddenAt: "2026-01-01T00:00:00Z", 218 + }) 219 + .run(); 220 + 221 + const res = await buildApp().request("/stats"); 222 + const body = (await res.json()) as { 223 + total: number; 224 + statusTypeCounts: Record<string, number>; 225 + }; 226 + 227 + expect(body.total).toBe(1); 228 + expect(body.statusTypeCounts.backlog).toBe(1); 229 + }); 230 + });
+35 -27
packages/kanban/src/api/stats.ts
··· 1 1 import { Hono } from "hono"; 2 2 import { getDb } from "@exosphere/core/db"; 3 - import { eq, and, sql, count, desc } from "@exosphere/core/db/drizzle"; 3 + import { eq, and, sql, desc } from "@exosphere/core/db/drizzle"; 4 4 import { collectCachedHandles, warmDidHandles } from "@exosphere/core/identity"; 5 5 import { tidToDate } from "@exosphere/core/pds"; 6 6 import type { AuthEnv } from "@exosphere/core/auth"; ··· 18 18 const app = new Hono<AuthEnv & SphereEnv>(); 19 19 20 20 // Dashboard stats: task counts grouped by StatusType + latest tasks. 21 - // Counts are computed by joining tasks with their column so we group by 22 - // the column's statusType, not by the column slug. One JOIN + GROUP BY. 21 + // Both queries LEFT JOIN `kanban_columns` on (sphereId, slug) — the count 22 + // query groups by the resolved statusType (so we can surface per-type totals), 23 + // and the latest query pulls statusType/label for rendering. LEFT JOIN so 24 + // orphan tasks (whose status slug no longer maps to a column) don't silently 25 + // drop from `total`; the .map() below filters them out of `latestTasks`. 23 26 app.get("/stats", async (c) => { 24 27 const db = getDb(); 25 28 const sphereId = c.var.sphereId; 26 29 30 + // LEFT JOIN so orphan tasks (status slug no longer maps to any column) 31 + // still appear: they collapse into a single null-statusType group whose 32 + // count feeds `total` but not `statusTypeCounts`. One query instead of two. 27 33 const countRows = db 28 34 .select({ 29 35 statusType: kanbanColumns.statusType, 30 36 count: sql<number>`count(${kanbanTasks.id})`.as("count"), 31 37 }) 32 38 .from(kanbanTasks) 33 - .innerJoin( 39 + .leftJoin( 34 40 kanbanColumns, 35 41 and( 36 42 eq(kanbanColumns.sphereId, kanbanTasks.sphereId), ··· 41 47 .groupBy(kanbanColumns.statusType) 42 48 .all(); 43 49 44 - // Count all tasks (including any orphans whose status slug no longer maps to a column) 45 - // so the total matches reality even if the join-based counts drop some rows. 46 - const totalRow = db 47 - .select({ total: count() }) 48 - .from(kanbanTasks) 49 - .where(and(eq(kanbanTasks.sphereId, sphereId), sql`${kanbanTasks.hiddenAt} is null`)) 50 - .get(); 51 - const total = totalRow?.total ?? 0; 52 - 53 50 const statusTypeCounts: Record<StatusType, number> = { 54 51 backlog: 0, 55 52 planned: 0, ··· 57 54 completed: 0, 58 55 canceled: 0, 59 56 }; 57 + let total = 0; 60 58 for (const row of countRows) { 61 - if (isStatusType(row.statusType)) { 59 + total += row.count; 60 + if (row.statusType && isStatusType(row.statusType)) { 62 61 statusTypeCounts[row.statusType] = row.count; 63 62 } 64 63 } ··· 75 74 updatedAt: kanbanTasks.updatedAt, 76 75 }) 77 76 .from(kanbanTasks) 78 - .innerJoin( 77 + .leftJoin( 79 78 kanbanColumns, 80 79 and( 81 80 eq(kanbanColumns.sphereId, kanbanTasks.sphereId), ··· 93 92 const { handles, toWarm } = collectCachedHandles(latestRows, (r) => r.authorDid); 94 93 if (toWarm.length) warmDidHandles(toWarm); 95 94 96 - const latestTasks = latestRows.map((r) => ({ 97 - id: r.id, 98 - number: r.number, 99 - title: r.title, 100 - status: r.status, 101 - statusType: r.statusType as StatusType, 102 - statusLabel: r.statusLabel, 103 - authorDid: r.authorDid, 104 - authorHandle: handles.get(r.authorDid) ?? null, 105 - createdAt: tidToDate(r.id), 106 - updatedAt: r.updatedAt, 107 - })); 95 + // Drop orphans (no matching column → null statusType/label); the UI can't 96 + // render them without a column. They still contribute to `total` above. 97 + // Rare case: if a recent task is orphaned, this list shows fewer than the 98 + // limit — acceptable trade-off vs. over-fetching on the common path. 99 + const latestTasks = latestRows 100 + .filter( 101 + (r): r is typeof r & { statusType: string; statusLabel: string } => 102 + r.statusType !== null && r.statusLabel !== null, 103 + ) 104 + .map((r) => ({ 105 + id: r.id, 106 + number: r.number, 107 + title: r.title, 108 + status: r.status, 109 + statusType: r.statusType as StatusType, 110 + statusLabel: r.statusLabel, 111 + authorDid: r.authorDid, 112 + authorHandle: handles.get(r.authorDid) ?? null, 113 + createdAt: tidToDate(r.id), 114 + updatedAt: r.updatedAt, 115 + })); 108 116 109 117 return c.json({ 110 118 total,
+5 -2
packages/kanban/src/db/schema.ts
··· 48 48 }, 49 49 (table) => [ 50 50 uniqueIndex("idx_kanban_tasks_sphere_number").on(table.sphereId, table.number), 51 - index("idx_kanban_tasks_sphere").on(table.sphereId), 52 - index("idx_kanban_tasks_status").on(table.status), 51 + // Serves board queries (sphere + status, ordered by position) and — via 52 + // leftmost prefix — sphere-only filters. Makes the old single-column 53 + // (sphere_id) and (status) indexes redundant; both have been dropped. 53 54 index("idx_kanban_tasks_sphere_status_position").on( 54 55 table.sphereId, 55 56 table.status, 56 57 table.position, 57 58 ), 59 + // Dashboard stats filter by sphere + visibility on every request. 60 + index("idx_kanban_tasks_sphere_hidden").on(table.sphereId, table.hiddenAt), 58 61 ], 59 62 ); 60 63
+75
scripts/fix-journal-ordering.ts
··· 1 + /** 2 + * Ensure `drizzle/meta/_journal.json` has strictly-monotonic `when` values. 3 + * 4 + * Why: drizzle's sqlite migrator skips any migration whose `when` is ≤ 5 + * `max(__drizzle_migrations.created_at)` on the target DB. If a newly-generated 6 + * migration's `when` (wall-clock `Date.now()` at generate-time) is smaller than 7 + * an earlier entry's — which happens here because a few older entries were 8 + * manually bumped into the future — the migrator silently skips the new one. 9 + * 10 + * This script walks the journal in `idx` order. Any entry whose `when` is not 11 + * strictly greater than the running max is rewritten to `max + 1`, so every 12 + * subsequent migration is guaranteed to be picked up. 13 + * 14 + * Runs idempotently — no-op if the journal is already monotonic. 15 + * 16 + * Usage: `bun run scripts/fix-journal-ordering.ts` (wired into `db:generate`). 17 + */ 18 + 19 + import { readFileSync, writeFileSync } from "node:fs"; 20 + import { join } from "node:path"; 21 + 22 + const ROOT = join(import.meta.dir, ".."); 23 + const JOURNAL_PATH = join(ROOT, "drizzle/meta/_journal.json"); 24 + 25 + interface JournalEntry { 26 + idx: number; 27 + version: string; 28 + when: number; 29 + tag: string; 30 + breakpoints: boolean; 31 + } 32 + 33 + interface Journal { 34 + version: string; 35 + dialect: string; 36 + entries: JournalEntry[]; 37 + } 38 + 39 + const raw = readFileSync(JOURNAL_PATH, "utf-8"); 40 + const journal: Journal = JSON.parse(raw); 41 + 42 + // Sort by idx to be defensive — the file is already idx-ordered but nothing 43 + // enforces that. 44 + const entries = [...journal.entries].sort((a, b) => a.idx - b.idx); 45 + 46 + const bumps: { tag: string; from: number; to: number }[] = []; 47 + let maxWhen = Number.NEGATIVE_INFINITY; 48 + for (const entry of entries) { 49 + if (entry.when > maxWhen) { 50 + maxWhen = entry.when; 51 + continue; 52 + } 53 + // Non-monotonic: bump to max + 1 so it's strictly greater than every prior. 54 + const next = maxWhen + 1; 55 + bumps.push({ tag: entry.tag, from: entry.when, to: next }); 56 + entry.when = next; 57 + maxWhen = next; 58 + } 59 + 60 + if (bumps.length === 0) { 61 + console.log("[fix-journal-ordering] journal already monotonic — nothing to do"); 62 + process.exit(0); 63 + } 64 + 65 + journal.entries = entries; 66 + // Preserve trailing newline — drizzle-kit writes one, and removing it churns diffs. 67 + const serialized = `${JSON.stringify(journal, null, 2)}\n`; 68 + writeFileSync(JOURNAL_PATH, serialized); 69 + 70 + console.log( 71 + `[fix-journal-ordering] bumped ${bumps.length} entr${bumps.length === 1 ? "y" : "ies"}:`, 72 + ); 73 + for (const b of bumps) { 74 + console.log(` ${b.tag}: ${b.from} → ${b.to}`); 75 + }