Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

chat: single-row News ticker + KPBJ stream moved to kpbj.fm

News ticker dropped from two rows to one; r8dio mini-player
repositioned to match the new ~12px height. KPBJ stream and
metadata endpoints updated from the dead kpbj.hasnoskills.com
host to stream.kpbj.fm + kpbj.fm status-json.xsl (Icecast).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+43 -65
+34 -59
system/public/aesthetic.computer/disks/chat.mjs
··· 293 293 }, 294 294 bj: { 295 295 label: "KPBJ", 296 - streamUrl: "https://kpbj.hasnoskills.com/listen/kpbj_test_station/radio.mp3", 296 + streamUrl: "https://stream.kpbj.fm/", 297 297 streamId: "chat-kpbj-stream", 298 - metadataUrl: "https://kpbj.hasnoskills.com/api/nowplaying/kpbj_test_station", 299 - parseTrack: (data) => data?.now_playing?.song?.text || "", 298 + metadataUrl: "https://www.kpbj.fm/api/stream/metadata", 299 + parseTrack: (data) => { 300 + const source = data?.icestats?.source; 301 + if (!source) return ""; 302 + if (source.artist && source.title) return `${source.artist} - ${source.title}`; 303 + return source.title || source.server_name || ""; 304 + }, 300 305 labelBg: [20, 30, 45], 301 306 labelBgHover: [35, 50, 70], 302 307 labelFg: [255, 200, 140], ··· 4635 4640 4636 4641 return colorCodedMessage; 4637 4642 } 4638 - // 📰 News ticker with TWO ROWS: headlines on top, activity/stats below 4639 - // Now fetches from news.aesthetic.computer API (see fetchNewsHeadlines) 4643 + // 📰 News ticker: single row of scrolling headlines 4644 + // Fetches from news.aesthetic.computer API (see fetchNewsHeadlines) 4640 4645 4641 4646 function paintNewsTicker($, theme) { 4642 4647 const { ink, screen, text, hud } = $; 4643 4648 const tickerCharWidth = 4; // MatrixChunky8 char width 4644 4649 const tickerHeight = 8; 4645 - const rowSpacing = 2; // Gap between rows 4646 4650 const rightMargin = 0; // Flush right, no margin 4647 - 4651 + 4648 4652 // "News" prefix styling - uniform width with r8Dio label 4649 4653 const newsPrefix = "News"; 4650 4654 const uniformLabelWidth = 28; // Fixed width to match both labels 4651 - 4652 - // Ticker dimensions - TWO ROWS. Width auto-expands to fill space 4655 + 4656 + // Ticker dimensions - SINGLE ROW. Width auto-expands to fill space 4653 4657 // between the HUD label and the right edge. 4654 4658 const tickerRight = screen.width - rightMargin; 4655 - const tickerY = 2; // Top row Y position 4656 - const row2Y = tickerY + tickerHeight + rowSpacing; // Second row Y 4657 - const totalTickerHeight = (tickerHeight * 2) + rowSpacing + 4; // Both rows + padding 4658 - 4659 + const tickerY = 2; // Row Y position 4660 + const totalTickerHeight = tickerHeight + 4; // Row + padding 4661 + 4659 4662 // Use dynamic news text (fetched from API or fallback) 4660 4663 const displayText = newsTickerText || "Report a story"; 4661 - const activityText = newsActivityText || "news.aesthetic.computer"; 4662 - 4664 + 4663 4665 // Seamless loop with separator (always scroll, even for fallback text) 4664 4666 const hasNews = newsHeadlines.length > 0; 4665 4667 const separator = " - "; 4666 4668 const loopText = displayText + separator; 4667 4669 const loopWidth = loopText.length * tickerCharWidth; 4668 - 4669 - // Activity row loop (scrolls same direction as headlines, but slower) 4670 - const activitySeparator = " · "; 4671 - const activityLoopText = activityText + activitySeparator; 4672 - const activityLoopWidth = activityLoopText.length * tickerCharWidth; 4673 - 4670 + 4674 4671 // Scroll animation (always scroll, slower for fallback text) 4675 4672 const scrollSpeed = hasNews ? 0.5 : 0.25; 4676 4673 const scrollOffset = (performance.now() * scrollSpeed / 16) % loopWidth; 4677 - // Activity scrolls same direction as headlines, but slower 4678 - const activityScrollSpeed = 0.25; 4679 - const activityScrollOffset = (performance.now() * activityScrollSpeed / 16) % activityLoopWidth; 4680 4674 4681 4675 // Calculate HUD label right edge to avoid overlap 4682 4676 // HUD label starts at x=6 and has width from hud.currentLabel() ··· 4693 4687 // Colors from theme 4694 4688 const handleColor = theme?.handle ? 4695 4689 (Array.isArray(theme.handle) ? theme.handle : [255, 150, 200]) : [255, 150, 200]; 4696 - const textColor = theme?.messageText ? 4690 + const textColor = theme?.messageText ? 4697 4691 (Array.isArray(theme.messageText) ? theme.messageText : [200, 200, 200]) : [200, 200, 200]; 4698 - const dimTextColor = [150, 150, 160]; // Dimmer for activity row 4699 - 4692 + 4700 4693 // "News" label - magenta background with white text (like aesthetic.news banner) 4701 4694 const newsBgColor = newsTickerHovered ? [200, 50, 150] : [180, 40, 130]; // Bright magenta 4702 4695 const newsFgColor = newsTickerHovered ? [255, 255, 255] : [255, 255, 255]; // White text ··· 4707 4700 // Calculate actual scrolling area width based on position 4708 4701 const actualTickerWidth = scrollAreaRight - scrollAreaLeft; 4709 4702 4710 - // Store bounds for click detection (entire ticker area including "News" label, both rows) 4703 + // Store bounds for click detection (entire ticker area including "News" label) 4711 4704 const totalWidth = uniformLabelWidth + actualTickerWidth + 1; 4712 4705 newsTickerBounds = { 4713 4706 x: newsBgX, ··· 4716 4709 h: totalTickerHeight, 4717 4710 }; 4718 4711 4719 - // Draw "News" label background - spans both rows 4712 + // Draw "News" label background 4720 4713 ink(...newsBgColor, 230).box(newsBgX, tickerY - 2, uniformLabelWidth + 1, totalTickerHeight); 4721 4714 // Center "News" text vertically in label area 4722 4715 const newsTextY = tickerY + Math.floor((totalTickerHeight - tickerHeight - 4) / 2); 4723 4716 const newsTextX = newsBgX + Math.floor((uniformLabelWidth - newsPrefix.length * tickerCharWidth) / 2); 4724 4717 ink(...newsFgColor).write(newsPrefix, { x: newsTextX, y: newsTextY }, undefined, undefined, false, "MatrixChunky8"); 4725 - 4726 - // Draw scrolling ticker background for both rows (use actual width) 4718 + 4719 + // Draw scrolling ticker background (use actual width) 4727 4720 ink(...scrollBgColor, 200).box(scrollAreaLeft, tickerY - 2, actualTickerWidth + 1, totalTickerHeight); 4728 - 4729 - // Draw subtle separator line between rows 4730 - ink(80, 50, 70, 150).box(scrollAreaLeft, row2Y - 1, actualTickerWidth, 1); 4731 - 4721 + 4732 4722 // Draw hover underline indicator (shows it's clickable) 4733 4723 if (newsTickerHovered) { 4734 4724 ink(255, 255, 255, 180).box(newsBgX, tickerY + totalTickerHeight - 1, totalWidth, 1); 4735 4725 } 4736 - 4737 - // Parse text for @handles to highlight (row 1) 4726 + 4727 + // Parse text for @handles to highlight 4738 4728 const handleRegex = /@[\w]+/g; 4739 4729 const handles = []; 4740 4730 let match; 4741 4731 while ((match = handleRegex.exec(loopText)) !== null) { 4742 4732 handles.push({ start: match.index, end: match.index + match[0].length, text: match[0] }); 4743 4733 } 4744 - 4745 - // ROW 1: Draw seamless looping HEADLINES with handle highlighting 4734 + 4735 + // Draw seamless looping HEADLINES with handle highlighting 4746 4736 for (let copy = 0; copy < 3; copy++) { 4747 4737 const baseX = scrollAreaLeft - scrollOffset + (copy * loopWidth); 4748 - 4738 + 4749 4739 for (let i = 0; i < loopText.length; i++) { 4750 4740 const charX = baseX + i * tickerCharWidth; 4751 - 4741 + 4752 4742 // Manual clip: only draw if within scroll area bounds 4753 4743 if (charX >= scrollAreaLeft && charX + tickerCharWidth <= scrollAreaRight) { 4754 4744 // Check if this character is part of a handle 4755 4745 const isHandle = handles.some(h => i >= h.start && i < h.end); 4756 4746 const charColor = isHandle ? handleColor : textColor; 4757 - 4747 + 4758 4748 ink(...charColor).write(loopText[i], { x: Math.round(charX), y: tickerY }, undefined, undefined, false, "MatrixChunky8"); 4759 - } 4760 - } 4761 - } 4762 - 4763 - // ROW 2: Draw ACTIVITY text (scrolls same direction as row 1, but slower) 4764 - for (let copy = 0; copy < 3; copy++) { 4765 - // Scroll left-to-right (same as row 1) 4766 - const baseX = scrollAreaLeft - activityScrollOffset + (copy * activityLoopWidth); 4767 - 4768 - for (let i = 0; i < activityLoopText.length; i++) { 4769 - const charX = baseX + i * tickerCharWidth; 4770 - 4771 - // Manual clip: only draw if within scroll area bounds 4772 - if (charX >= scrollAreaLeft && charX + tickerCharWidth <= scrollAreaRight) { 4773 - ink(...dimTextColor).write(activityLoopText[i], { x: Math.round(charX), y: row2Y }, undefined, undefined, false, "MatrixChunky8"); 4774 4749 } 4775 4750 } 4776 4751 } ··· 4796 4771 const uniformLabelWidth = 28; 4797 4772 4798 4773 // Match news ticker height so we can sit directly beneath it without overlap. 4799 - // News ticker total height = (tickerHeight * 2) + rowSpacing(2) + 4 = 22, drawn from y=0. 4800 - const newsTotalHeight = (tickerHeight * 2) + 2 + 4; 4774 + // News ticker total height = tickerHeight + 4 = 12, drawn from y=0. 4775 + const newsTotalHeight = tickerHeight + 4; 4801 4776 const tickerRight = screen.width - rightMargin; 4802 4777 const tickerY = newsTotalHeight + 4; // 4px gap below news ticker 4803 4778
+9 -6
system/public/aesthetic.computer/disks/kpbj.mjs
··· 1 1 // kpbj, 2026.02.01 2 2 // 📻 KPBJ.FM live stream player - Shadow Hills Community Radio 3 - // Stream: https://kpbj.hasnoskills.com/listen/kpbj_test_station/radio.mp3 3 + // Stream: https://stream.kpbj.fm/ 4 4 5 5 /* #region 🏁 TODO 6 6 - [ ] Test on mobile/iOS ··· 27 27 28 28 // KPBJ Configuration 29 29 const CONFIG = { 30 - streamUrl: "https://kpbj.hasnoskills.com/listen/kpbj_test_station/radio.mp3", 30 + streamUrl: "https://stream.kpbj.fm/", 31 31 streamId: "kpbj-stream", 32 - metadataUrl: "https://kpbj.hasnoskills.com/api/nowplaying/kpbj_test_station", 32 + metadataUrl: "https://www.kpbj.fm/api/stream/metadata", 33 33 playoutNowUrl: "https://kpbj.fm/api/playout/now", 34 34 playoutFallbackUrl: "https://kpbj.fm/api/playout/fallback", 35 35 qrUrl: "https://prompt.ac/kpbj", ··· 175 175 const response = await fetch(CONFIG.metadataUrl); 176 176 if (response.ok) { 177 177 const data = await response.json(); 178 - // Handle AzuraCast format 179 - if (data.now_playing && data.now_playing.song) { 180 - state.currentTrack = data.now_playing.song.title || data.now_playing.song.text || ""; 178 + // Icecast status-json.xsl: { icestats: { source: { title, artist? } } } 179 + const source = data?.icestats?.source; 180 + if (source) { 181 + state.currentTrack = source.artist && source.title 182 + ? `${source.artist} - ${source.title}` 183 + : (source.title || ""); 181 184 } 182 185 } 183 186 } catch (err) {