|
36 | 36 | let deleteError = $state<string | null>(null); |
37 | 37 | let deleteSuccess = $state<string | null>(null); |
38 | 38 |
|
| 39 | + // Publish state |
| 40 | + let showPublishForm = $state(false); |
| 41 | + let publishChannel = $state("task-requests"); |
| 42 | + let publishPayload = $state('{"message": "Please report what time it is where you are"}'); |
| 43 | + let publishMessageType = $state(""); |
| 44 | + let isPublishing = $state(false); |
| 45 | + let publishError = $state<string | null>(null); |
| 46 | + let publishSuccess = $state<string | null>(null); |
| 47 | +
|
39 | 48 | let filteredChannels = $derived.by(() => { |
40 | 49 | if (!channels.length) return []; |
41 | 50 | if (!searchQuery.trim()) return channels; |
|
205 | 214 | } |
206 | 215 | } |
207 | 216 |
|
| 217 | + async function publishMessage() { |
| 218 | + if (!publishChannel.trim()) return; |
| 219 | +
|
| 220 | + let parsedPayload: any; |
| 221 | + try { |
| 222 | + parsedPayload = JSON.parse(publishPayload); |
| 223 | + } catch { |
| 224 | + publishError = "Invalid JSON payload"; |
| 225 | + return; |
| 226 | + } |
| 227 | +
|
| 228 | + try { |
| 229 | + isPublishing = true; |
| 230 | + publishError = null; |
| 231 | + publishSuccess = null; |
| 232 | +
|
| 233 | + const body: any = { payload: parsedPayload }; |
| 234 | + if (publishMessageType.trim()) { |
| 235 | + body.message_type = publishMessageType.trim(); |
| 236 | + } |
| 237 | +
|
| 238 | + const response = await fetch( |
| 239 | + `/api/signal/channels/${encodeURIComponent(publishChannel.trim())}/messages`, |
| 240 | + { |
| 241 | + method: "POST", |
| 242 | + headers: { "Content-Type": "application/json" }, |
| 243 | + body: JSON.stringify(body), |
| 244 | + }, |
| 245 | + ); |
| 246 | +
|
| 247 | + if (!response.ok) { |
| 248 | + const errorData = await response.json().catch(() => ({})); |
| 249 | + throw new Error(errorData.error || `Failed (${response.status})`); |
| 250 | + } |
| 251 | +
|
| 252 | + publishSuccess = `Message published to "${publishChannel.trim()}"`; |
| 253 | + await fetchChannels(); |
| 254 | + } catch (err) { |
| 255 | + publishError = |
| 256 | + err instanceof Error ? err.message : "Failed to publish message"; |
| 257 | + } finally { |
| 258 | + isPublishing = false; |
| 259 | + } |
| 260 | + } |
| 261 | +
|
208 | 262 | onMount(() => { |
209 | 263 | fetchChannels(); |
210 | 264 | }); |
|
225 | 279 | </div> |
226 | 280 | </div> |
227 | 281 | <div class="header-controls"> |
228 | | - <button |
229 | | - class="refresh-button" |
230 | | - onclick={fetchChannels} |
231 | | - disabled={isLoading} |
232 | | - aria-label="Refresh signal channels" |
233 | | - > |
234 | | - <svg |
235 | | - class="refresh-icon" |
236 | | - class:spinning={isLoading} |
237 | | - xmlns="http://www.w3.org/2000/svg" |
238 | | - viewBox="0 0 24 24" |
239 | | - fill="none" |
240 | | - stroke="currentColor" |
241 | | - stroke-width="2" |
242 | | - stroke-linecap="round" |
243 | | - stroke-linejoin="round" |
| 282 | + <div class="header-buttons"> |
| 283 | + <button |
| 284 | + class="publish-button" |
| 285 | + onclick={() => { showPublishForm = !showPublishForm; publishError = null; publishSuccess = null; }} |
| 286 | + > |
| 287 | + {showPublishForm ? "Cancel" : "Publish Message"} |
| 288 | + </button> |
| 289 | + <button |
| 290 | + class="refresh-button" |
| 291 | + onclick={fetchChannels} |
| 292 | + disabled={isLoading} |
| 293 | + aria-label="Refresh signal channels" |
244 | 294 | > |
245 | | - <path |
246 | | - d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2" |
247 | | - /> |
248 | | - </svg> |
249 | | - {isLoading ? "Refreshing..." : "Refresh"} |
250 | | - </button> |
| 295 | + <svg |
| 296 | + class="refresh-icon" |
| 297 | + class:spinning={isLoading} |
| 298 | + xmlns="http://www.w3.org/2000/svg" |
| 299 | + viewBox="0 0 24 24" |
| 300 | + fill="none" |
| 301 | + stroke="currentColor" |
| 302 | + stroke-width="2" |
| 303 | + stroke-linecap="round" |
| 304 | + stroke-linejoin="round" |
| 305 | + > |
| 306 | + <path |
| 307 | + d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2" |
| 308 | + /> |
| 309 | + </svg> |
| 310 | + {isLoading ? "Refreshing..." : "Refresh"} |
| 311 | + </button> |
| 312 | + </div> |
251 | 313 | {#if lastUpdated} |
252 | 314 | <div class="last-updated"> |
253 | 315 | Last updated: <span class="timestamp">{lastUpdated}</span> |
|
256 | 318 | </div> |
257 | 319 | </div> |
258 | 320 | </div> |
| 321 | + {#if showPublishForm} |
| 322 | + <div class="publish-form"> |
| 323 | + <div class="publish-row"> |
| 324 | + <div class="publish-field"> |
| 325 | + <label class="publish-label" for="pub-channel">Channel</label> |
| 326 | + <input |
| 327 | + id="pub-channel" |
| 328 | + type="text" |
| 329 | + class="publish-input" |
| 330 | + bind:value={publishChannel} |
| 331 | + disabled={isPublishing} |
| 332 | + /> |
| 333 | + </div> |
| 334 | + <div class="publish-field"> |
| 335 | + <label class="publish-label" for="pub-type">Type <span class="optional">(optional)</span></label> |
| 336 | + <input |
| 337 | + id="pub-type" |
| 338 | + type="text" |
| 339 | + class="publish-input" |
| 340 | + placeholder="e.g. task-request" |
| 341 | + bind:value={publishMessageType} |
| 342 | + disabled={isPublishing} |
| 343 | + /> |
| 344 | + </div> |
| 345 | + </div> |
| 346 | + <div class="publish-field"> |
| 347 | + <label class="publish-label" for="pub-payload">Payload (JSON)</label> |
| 348 | + <textarea |
| 349 | + id="pub-payload" |
| 350 | + class="publish-textarea" |
| 351 | + bind:value={publishPayload} |
| 352 | + disabled={isPublishing} |
| 353 | + rows="4" |
| 354 | + ></textarea> |
| 355 | + </div> |
| 356 | + {#if publishError} |
| 357 | + <div class="alert alert-error">{publishError}</div> |
| 358 | + {/if} |
| 359 | + {#if publishSuccess} |
| 360 | + <div class="alert alert-success">{publishSuccess}</div> |
| 361 | + {/if} |
| 362 | + <button |
| 363 | + class="btn-publish" |
| 364 | + onclick={publishMessage} |
| 365 | + disabled={isPublishing || !publishChannel.trim()} |
| 366 | + > |
| 367 | + {isPublishing ? "Publishing..." : "Publish"} |
| 368 | + </button> |
| 369 | + </div> |
| 370 | + {/if} |
| 371 | + |
259 | 372 | <div class="panel-content"> |
260 | 373 | <!-- Status Messages --> |
261 | 374 | {#if deleteSuccess} |
|
541 | 654 | color: var(--color-surface-300); |
542 | 655 | } |
543 | 656 |
|
| 657 | + .header-buttons { |
| 658 | + display: flex; |
| 659 | + gap: 0.5rem; |
| 660 | + } |
| 661 | +
|
| 662 | + .publish-button { |
| 663 | + display: flex; |
| 664 | + align-items: center; |
| 665 | + gap: 0.5rem; |
| 666 | + padding: 0.5rem 1rem; |
| 667 | + background: #51b265; |
| 668 | + color: white; |
| 669 | + border: none; |
| 670 | + border-radius: 0.375rem; |
| 671 | + font-size: 0.875rem; |
| 672 | + font-weight: 500; |
| 673 | + cursor: pointer; |
| 674 | + transition: background-color 0.2s; |
| 675 | + } |
| 676 | +
|
| 677 | + .publish-button:hover { |
| 678 | + background: #3d9e52; |
| 679 | + } |
| 680 | +
|
| 681 | + :global([data-mode="dark"]) .publish-button { |
| 682 | + background: #51b265; |
| 683 | + } |
| 684 | +
|
| 685 | + :global([data-mode="dark"]) .publish-button:hover { |
| 686 | + background: #3d9e52; |
| 687 | + } |
| 688 | +
|
| 689 | + .publish-form { |
| 690 | + padding: 1rem 1.5rem; |
| 691 | + background: #f9fafb; |
| 692 | + border-bottom: 1px solid #e5e7eb; |
| 693 | + display: flex; |
| 694 | + flex-direction: column; |
| 695 | + gap: 0.75rem; |
| 696 | + } |
| 697 | +
|
| 698 | + :global([data-mode="dark"]) .publish-form { |
| 699 | + background: rgb(var(--color-surface-900)); |
| 700 | + border-bottom-color: rgb(var(--color-surface-700)); |
| 701 | + } |
| 702 | +
|
| 703 | + .publish-row { |
| 704 | + display: flex; |
| 705 | + gap: 0.75rem; |
| 706 | + } |
| 707 | +
|
| 708 | + .publish-field { |
| 709 | + display: flex; |
| 710 | + flex-direction: column; |
| 711 | + gap: 0.25rem; |
| 712 | + flex: 1; |
| 713 | + } |
| 714 | +
|
| 715 | + .publish-label { |
| 716 | + font-size: 0.75rem; |
| 717 | + font-weight: 600; |
| 718 | + color: #374151; |
| 719 | + } |
| 720 | +
|
| 721 | + :global([data-mode="dark"]) .publish-label { |
| 722 | + color: var(--color-surface-300); |
| 723 | + } |
| 724 | +
|
| 725 | + .publish-label .optional { |
| 726 | + font-weight: 400; |
| 727 | + color: #9ca3af; |
| 728 | + } |
| 729 | +
|
| 730 | + .publish-input { |
| 731 | + padding: 0.5rem 0.75rem; |
| 732 | + border: 1px solid #d1d5db; |
| 733 | + border-radius: 0.375rem; |
| 734 | + font-size: 0.875rem; |
| 735 | + font-family: monospace; |
| 736 | + } |
| 737 | +
|
| 738 | + .publish-input:focus { |
| 739 | + outline: none; |
| 740 | + border-color: #3b82f6; |
| 741 | + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); |
| 742 | + } |
| 743 | +
|
| 744 | + :global([data-mode="dark"]) .publish-input { |
| 745 | + background: rgb(var(--color-surface-700)); |
| 746 | + border-color: rgb(var(--color-surface-600)); |
| 747 | + color: var(--color-surface-100); |
| 748 | + } |
| 749 | +
|
| 750 | + .publish-textarea { |
| 751 | + padding: 0.5rem 0.75rem; |
| 752 | + border: 1px solid #d1d5db; |
| 753 | + border-radius: 0.375rem; |
| 754 | + font-size: 0.8125rem; |
| 755 | + font-family: monospace; |
| 756 | + resize: vertical; |
| 757 | + } |
| 758 | +
|
| 759 | + .publish-textarea:focus { |
| 760 | + outline: none; |
| 761 | + border-color: #3b82f6; |
| 762 | + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); |
| 763 | + } |
| 764 | +
|
| 765 | + :global([data-mode="dark"]) .publish-textarea { |
| 766 | + background: rgb(var(--color-surface-700)); |
| 767 | + border-color: rgb(var(--color-surface-600)); |
| 768 | + color: var(--color-surface-100); |
| 769 | + } |
| 770 | +
|
| 771 | + .btn-publish { |
| 772 | + align-self: flex-start; |
| 773 | + padding: 0.5rem 1.5rem; |
| 774 | + background: #51b265; |
| 775 | + color: white; |
| 776 | + border: none; |
| 777 | + border-radius: 0.375rem; |
| 778 | + font-size: 0.875rem; |
| 779 | + font-weight: 600; |
| 780 | + cursor: pointer; |
| 781 | + transition: background-color 0.2s; |
| 782 | + } |
| 783 | +
|
| 784 | + .btn-publish:hover:not(:disabled) { |
| 785 | + background: #3d9e52; |
| 786 | + } |
| 787 | +
|
| 788 | + .btn-publish:disabled { |
| 789 | + opacity: 0.6; |
| 790 | + cursor: not-allowed; |
| 791 | + } |
| 792 | +
|
544 | 793 | .panel-content { |
545 | 794 | padding: 1.5rem; |
546 | 795 | } |
|
0 commit comments