1. <script lang="ts">
  2. import { onMount } from 'svelte';
  3. import { tick } from 'svelte';
  4.  
  5. interface Destination { destination: string; destination_hexid: string; }
  6. interface RouteItem {
  7. route_number: string | number;
  8. pickup_point: { pickup_point: string; pickup_hexid: string };
  9. destinations: Destination[];
  10. }
  11.  
  12. let routes: RouteItem[] = [];
  13. let scored: any[] = [];
  14. let isLoading = true;
  15. let error: string | null = null;
  16.  
  17. const TOKENS = {
  18. accent: '#00f2ff',
  19. accentGlobal: '#0ff'
  20. };
  21.  
  22. function getTimeState() {
  23. const hour = new Date().getHours();
  24. if (hour >= 6 && hour <= 9 || hour >= 16 && hour <= 20) return 'peak';
  25. if (hour >= 10 && hour <= 15) return 'mid';
  26. return 'off';
  27. }
  28.  
  29. async function fetchRoutes() {
  30. try {
  31. const json = await res.json();
  32. routes = json.non_null_objects || json;
  33. } catch (e) {
  34. error = 'Network Sync Failed';
  35. } finally {
  36. isLoading = false;
  37. }
  38. }
  39.  
  40. function routeScore(num: string | number): number {
  41. const n = parseInt(String(num).replace(/\D/g, '') || '7', 10);
  42. return parseFloat((0.6 + ((n * 17 + 13) % 37) / 100).toFixed(2));
  43. }
  44.  
  45. function bayesScore(route: RouteItem, alpha = 2, beta = 2) {
  46. const prior = routeScore(route.route_number);
  47. const timeFactor = getTimeState() === 'peak' ? 1.2 : 0.8;
  48. const loadFactor = (route.destinations?.length || 1) / 10;
  49. const posterior = (alpha + prior * timeFactor) / (alpha + beta + loadFactor);
  50. return Math.min(Math.max(posterior, 0), 1);
  51. }
  52.  
  53. function classify(s: number) {
  54. if (s >= 0.85) return 'great';
  55. if (s >= 0.72) return 'good';
  56. if (s >= 0.58) return 'warn';
  57. return 'low';
  58. }
  59.  
  60. function label(cls: string) {
  61. return cls === 'great' ? 'Optimal' : cls === 'good' ? 'Stable' : cls === 'warn' ? 'Moderate' : 'Congested';
  62. }
  63.  
  64. function getGradient(cls: string, posterior: number) {
  65. const base = cls === 'great' ? '#0ff' : cls === 'good' ? '#0f0' : cls === 'warn' ? '#ff0' : '#f00';
  66. const secondary = cls === 'great' ? '#00cfff' : cls === 'good' ? '#00aa00' : cls === 'warn' ? '#ffaa00' : '#ff3300';
  67. const intensity = Math.floor(posterior * 100);
  68. return `linear-gradient(90deg, ${base} ${intensity}%, ${secondary} 100%)`;
  69. }
  70.  
  71. function updateScores() {
  72. scored = routes.map(r => {
  73. let posterior = bayesScore(r) + (Math.random() - 0.5) * 0.05; // small fluctuation
  74.  
  75. // exaggerate some routes randomly for visual drama
  76. if (Math.random() < 0.2) posterior *= 1.2;
  77.  
  78. posterior = Math.min(Math.max(posterior, 0), 1);
  79.  
  80. const cls = classify(posterior);
  81. return { ...r, posterior, cls, label: label(cls), accent: TOKENS.accent };
  82. });
  83. }
  84.  
  85. onMount(async () => {
  86. await fetchRoutes();
  87. updateScores();
  88. const interval = setInterval(() => updateScores(), 2000); // dynamic updates
  89. return () => clearInterval(interval);
  90. });
  91. </script>
  92.  
  93. <div class="os">
  94. <header class="header">
  95. <div class="brand">
  96. <div class="pulse" style="background:{TOKENS.accentGlobal}; box-shadow:0 0 10px {TOKENS.accentGlobal};"></div>
  97. <h1>TRANSIT OS DASHBOARD</h1>
  98. </div>
  99. <div class="iq">
  100. <svg viewBox="0 0 36 36">
  101. <circle cx="18" cy="18" r="15.9155" fill="none" stroke="#222" stroke-width="2.5"/>
  102. <circle cx="18" cy="18" r="15.9155" fill="none" stroke="{TOKENS.accentGlobal}" stroke-width="2.5"
  103. stroke-dasharray="{scored.length ? scored.reduce((a,r)=>a.posterior + a,0)/scored.length*100 : 0},100"
  104. stroke-linecap="round"/>
  105. </svg>
  106. <span>{scored.length ? ((scored.reduce((a,r)=>a.posterior+a,0)/scored.length)*100).toFixed(1) : '0.0'}</span>
  107. </div>
  108. </header>
  109.  
  110. <div class="viewport">
  111. {#if isLoading}
  112. <div class="loader">
  113. <div class="scan" style="background:{TOKENS.accentGlobal}; box-shadow:0 0 10px {TOKENS.accentGlobal};"></div>
  114. <p>LOADING ROUTE DATA...</p>
  115. </div>
  116. {:else if error}
  117. <p>{error}</p>
  118. {:else}
  119. {#each scored as r (r.route_number)}
  120. <div class="card" style="--accent:{r.accent}">
  121. <div>
  122. <div class="route">{r.route_number}</div>
  123. <div class="nodes">{r.destinations?.length || 0} NODES</div>
  124. </div>
  125. <div class="mid">
  126. <div class="origin">{r.pickup_point?.pickup_point || 'TERMINUS'}</div>
  127. <div class="bar">
  128. <div class="fill" style="width:{r.posterior*100}%; background:{getGradient(r.cls,r.posterior)};"></div>
  129. </div>
  130. </div>
  131. <div class="right">
  132. <div class="tag">{r.label}</div>
  133. <div class="pct">{(r.posterior*100).toFixed(0)}%</div>
  134. </div>
  135. </div>
  136. {/each}
  137. {/if}
  138. </div>
  139.  
  140. <footer class="footer">
  141. AUTHENTICATED ACCESS: DYNAMIC BAYES v2.0
  142. </footer>
  143. </div>
  144.  
  145. <style>
  146. .os {
  147. --bg: #050505;
  148. --glass: rgba(255,255,255,0.05);
  149. --border: rgba(255,255,255,0.08);
  150. height: 100%;
  151. display: flex;
  152. flex-direction: column;
  153. background: radial-gradient(circle at top, #111, #050505);
  154. color: white;
  155. font-family: 'JetBrains Mono', monospace;
  156. }
  157. .header {
  158. display: flex;
  159. justify-content: space-between;
  160. padding: 20px;
  161. backdrop-filter: blur(12px);
  162. background: var(--glass);
  163. border-bottom: 1px solid var(--border);
  164. }
  165. .brand { display: flex; gap: 10px; align-items: center; }
  166. .brand h1 { font-size: 0.9rem; letter-spacing: 0.2em; color: var(--accent-global); }
  167. .pulse { width: 8px; height: 8px; background: var(--accent-global); border-radius: 50%; box-shadow: 0 0 10px var(--accent-global); animation: pulse 1.5s infinite; }
  168. @keyframes pulse { 50% { transform: scale(1.5); opacity: 0.5; } }
  169. .iq { width: 50px; position: relative; }
  170. .iq svg { transform: rotate(-90deg); }
  171. .iq span { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 0.7rem; }
  172. .viewport { flex: 1; overflow: auto; padding: 20px; }
  173. .card { display: flex; gap: 16px; padding: 14px; margin-bottom: 10px; backdrop-filter: blur(14px); background: var(--glass); border: 1px solid var(--border); border-radius: 10px; position: relative; overflow: hidden; transition: 0.3s; }
  174. .card::before { content: ""; position: absolute; inset: 0; border-radius: 10px; background: linear-gradient(120deg, transparent, var(--accent), transparent); opacity: 0.4; }
  175. .card:hover { transform: translateY(-3px); box-shadow: 0 0 20px var(--accent); }
  176. .route { font-weight: 900; font-size: 1.1rem; }
  177. .nodes { font-size: 0.6rem; color: #666; }
  178. .mid { flex: 1; }
  179. .origin { font-size: 0.7rem; color: #aaa; margin-bottom: 6px; }
  180. .bar { height: 4px; background: #222; border-radius: 2px; overflow: hidden; }
  181. .fill {
  182. height: 100%;
  183. position: relative;
  184. overflow: hidden;
  185. background-size: 200% 100%;
  186. animation: flow 3s linear infinite;
  187. border-radius: 2px;
  188. transition: width 0.8s ease-in-out; /* Smooth progressive bar */
  189. }
  190. .fill::after { content:""; position:absolute; inset:0; background: linear-gradient(120deg, rgba(255,255,255,0) 20%, rgba(255,255,255,0.3) 50%, rgba(255,255,255,0) 80%); animation: sweep 2s linear infinite; }
  191. @keyframes flow { 0% { background-position:0 0; } 100% { background-position:200% 0; } }
  192. @keyframes sweep { 0% { transform:translateX(-100%);} 100% { transform:translateX(100%);} }
  193. .card:hover .fill { filter: brightness(1.4) saturate(1.2); }
  194. .right { text-align: right; }
  195. .tag { font-size: 0.6rem; background: rgba(255,255,255,0.06); padding: 2px 6px; border-radius: 6px; }
  196. .pct { font-size: 1rem; font-weight: 900; }
  197. .loader { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; }
  198. .scan { width: 100px; height: 2px; background: var(--accent-global); box-shadow: 0 0 10px var(--accent-global); animation: scan 2s infinite; }
  199. @keyframes scan { 50% { transform: translateY(20px); opacity: 1; } 0%,100% { transform: translateY(-20px); opacity: 0; } }
  200. .footer { padding: 10px 20px; font-size: 0.55rem; color: #555; border-top: 1px solid #111; }
  201. </style>