Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
75 kB · 2299 lines
HTML
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300<!DOCTYPE html><html><head> <title>Builds - false.work</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <link rel="icon" href="/favicon.svg" type="image/svg+xml"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"> <meta http-equiv="Pragma" content="no-cache"> <meta http-equiv="Expires" content="0"> <style> :root { /* Dark theme (default) */ --bg-primary: #000; --bg-secondary: #0a0a0a; --bg-card: #111; --bg-input: #0a0a0a; --bg-input-focus: #111; --bg-button: #222; --bg-button-hover: #333; --text-primary: #fff; --text-secondary: #888; --text-muted: #666; --border-primary: #333; --border-secondary: #444; --border-input: #333; --border-input-focus: #555; --accent: #fa0; --accent-secondary: #09f; --platform-windows: #09f; --platform-mac: #888; --platform-ios: #f0f; --error: #f66; --card-windows-bg: linear-gradient(135deg, #0a0a0a 0%, #0a1a2a 100%); --card-mac-bg: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%); --card-ios-bg: linear-gradient(135deg, #0a0a0a 0%, #1a0a1a 100%); --ios-note-bg: #1a0a1a; --tag-bg: #222; --fruit-color: #0f0; --side-color: #09f; --big-color: #f0f; --font-mono: 'Courier New', Consolas, monospace; --font-sans: Arial, Helvetica, sans-serif; }
@media (prefers-color-scheme: light) { :root { --bg-primary: #fff; --bg-secondary: #fff; --bg-card: #fafafa; --bg-input: #fff; --bg-input-focus: #fafafa; --bg-button: #e8e8e8; --bg-button-hover: #ddd; --text-primary: #111; --text-secondary: #555; --text-muted: #888; --border-primary: #ddd; --border-secondary: #ccc; --border-input: #ccc; --border-input-focus: #999; --accent: #d90; --accent-secondary: #07d; --platform-windows: #07d; --platform-mac: #666; --platform-ios: #c0c; --error: #c33; --card-windows-bg: linear-gradient(135deg, #fff 0%, #e8f4ff 100%); --card-mac-bg: linear-gradient(135deg, #fff 0%, #f0f0f0 100%); --card-ios-bg: linear-gradient(135deg, #fff 0%, #ffe8ff 100%); --ios-note-bg: #fef0fe; --tag-bg: #f0f0f0; --fruit-color: #080; --side-color: #07d; --big-color: #c0c; } .logo { filter: invert(1); } }
* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: var(--font-mono); background: var(--bg-primary); color: var(--text-primary); line-height: 1.6; padding: 2rem; padding: env(safe-area-inset-top, 2rem) env(safe-area-inset-right, 2rem) env(safe-area-inset-bottom, 2rem) env(safe-area-inset-left, 2rem); } .container { max-width: 800px; margin: 0 auto; } #auth-container { max-width: 400px; margin: 10rem auto; text-align: center; } #auth-container h1 { margin-bottom: 2rem; } input[type="password"] { background: var(--bg-input); border: 2px solid var(--border-input); color: var(--text-primary); font-family: 'Courier New', monospace; font-size: 1rem; padding: 0.75rem; width: 100%; margin-bottom: 1rem; } input[type="password"]:focus { outline: none; border-color: var(--border-input-focus); background: var(--bg-input-focus); } button { background: var(--bg-button); color: var(--text-primary); border: 2px solid var(--border-secondary); font-family: 'Courier New', monospace; font-size: 1rem; font-weight: bold; padding: 0.75rem 2rem; cursor: pointer; transition: all 0.2s; } button:hover { background: var(--bg-button-hover); border-color: var(--text-muted); } .error { color: var(--error); margin-top: 1rem; } #content { display: none; } body.authenticated #content { display: block; } h1 { font-size: 1.5rem; margin-bottom: 1.5rem; color: var(--text-primary); } h2 { color: var(--accent); margin-bottom: 1rem; } .platform-filters { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; flex-wrap: wrap; } .platform-filters button { padding: 0.5rem 1rem; font-size: 0.9rem; border-width: 1px; } .platform-filters button.active { background: var(--bg-button-hover); border-color: var(--accent); color: var(--accent); } .platform-filters button.disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; } .platform-tag { display: inline-block; padding: 0.2rem 0.5rem; margin-right: 0.5rem; background: var(--tag-bg); border: 1px solid var(--border-secondary); border-radius: 3px; font-size: 0.75rem; font-weight: bold; text-transform: uppercase; } .platform-windows { color: var(--platform-windows); border-color: var(--platform-windows); } .platform-mac { color: var(--platform-mac); border-color: var(--platform-mac); } .platform-ios { color: var(--platform-ios); border-color: var(--platform-ios); } .platform-plugin { color: var(--accent-secondary); border-color: var(--accent-secondary); } .project-name { color: var(--accent); font-weight: bold; margin-right: 0.5rem; } a { color: var(--text-primary); text-decoration: none; font-weight: bold; } a:hover { text-decoration: underline; } .meta { color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem; } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--border-primary); text-align: center; color: var(--text-muted); font-size: 0.85rem; } footer a { color: var(--text-secondary); font-weight: normal; } .build-card { padding: 1.5rem; border-radius: 4px; margin-bottom: 1rem; } .build-card.windows { background: var(--card-windows-bg); border-left: 4px solid var(--platform-windows); } .build-card.mac { background: var(--card-mac-bg); border-left: 4px solid var(--platform-mac); } .build-card.ios { background: var(--card-ios-bg); border-left: 4px solid var(--platform-ios); } .plugin-card { padding: 1.25rem 1.5rem; border-radius: 4px; margin-bottom: 1rem; background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-secondary) 100%); border-left: 4px solid var(--accent-secondary); } .plugin-header { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.5rem; } .plugin-name { color: var(--accent-secondary); font-weight: bold; font-size: 1.05rem; } .plugin-version { color: var(--text-muted); font-family: var(--font-mono); font-size: 0.85rem; } .plugin-engine-tag { display: inline-block; padding: 0.15rem 0.5rem; background: var(--tag-bg); color: var(--text-secondary); border-radius: 3px; font-size: 0.75rem; font-family: var(--font-mono); } .plugin-description { color: var(--text-secondary); font-size: 0.9rem; margin: 0.5rem 0 0.75rem 0; line-height: 1.4; } .plugin-artifacts { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; } .plugin-artifact-btn { display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.4rem 0.75rem; background: var(--bg-button); color: var(--text-primary) !important; border: 1px solid var(--border-primary); border-radius: 3px; font-size: 0.85rem; font-weight: 500 !important; text-decoration: none !important; transition: all 0.15s; } .plugin-artifact-btn:hover { background: var(--bg-button-hover); border-color: var(--accent-secondary); } .plugin-artifact-btn .size { color: var(--text-muted); font-size: 0.75rem; font-family: var(--font-mono); } .plugin-upstream { font-size: 0.8rem; color: var(--text-muted); margin-top: 0.5rem; } .plugin-upstream a { color: var(--text-secondary); font-weight: normal; } .build-header { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; } .date-header { margin: 2rem 0 1rem 0; font-size: 1.1rem; } .date-header.today { color: var(--accent); } .date-header.previous { color: var(--text-muted); } .ios-install-btn { display: inline-block; background: linear-gradient(135deg, var(--platform-ios) 0%, #90f 100%); color: white !important; padding: 0.5rem 1rem; border-radius: 4px; text-decoration: none !important; font-weight: bold; font-size: 0.9rem; margin-left: 0.5rem; transition: all 0.2s; border: 1px solid var(--platform-ios); } .ios-install-btn:hover { background: linear-gradient(135deg, #f5f 0%, #a5f 100%); transform: translateY(-1px); box-shadow: 0 2px 8px rgba(255, 0, 255, 0.3); } .ios-note { background: var(--ios-note-bg); border-left: 2px solid var(--platform-ios); padding: 0.5rem 1rem; margin-top: 0.5rem; border-radius: 3px; font-size: 0.85rem; color: var(--text-secondary); } .loading { text-align: center; padding: 3rem; color: var(--text-muted); } .loading::after { content: ''; animation: dots 1.5s steps(4, end) infinite; } @keyframes dots { 0%, 20% { content: ''; } 40% { content: '.'; } 60% { content: '..'; } 80%, 100% { content: '...'; } } /* Failed/stale build styles */ .build-card.failed { position: relative; opacity: 0.6; } .build-card.failed::before { content: '✕'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 4rem; color: var(--error); opacity: 0.4; pointer-events: none; font-weight: bold; } .build-card.failed .build-header a { text-decoration: line-through; color: var(--text-muted); } .status-badge { display: inline-block; padding: 0.15rem 0.4rem; margin-left: 0.5rem; background: var(--error); color: #fff; border-radius: 3px; font-size: 0.7rem; font-weight: bold; text-transform: uppercase; } .status-badge.stale { background: #a50; } .status-badge.failed { background: var(--error); } .status-reason { color: var(--error); font-size: 0.85rem; margin-top: 0.5rem; font-style: italic; } /* Build tags */ .build-tags { display: inline-flex; gap: 0.3rem; margin-left: 0.5rem; } .build-tag { display: inline-block; padding: 0.15rem 0.4rem; background: var(--tag-bg); border: 1px solid var(--border-secondary); border-radius: 3px; font-size: 0.65rem; font-weight: bold; text-transform: uppercase; letter-spacing: 0.5px; } .build-tag.ultra { background: linear-gradient(135deg, #f0f 0%, #90f 100%); border-color: #f0f; color: #fff; } .build-tag.cinematic { background: linear-gradient(135deg, #fa0 0%, #f60 100%); border-color: #fa0; color: #000; } .build-tag.high, .build-tag.epic { background: linear-gradient(135deg, #0f0 0%, #0a0 100%); border-color: #0f0; color: #000; } .build-tag.medium { background: #555; border-color: #666; color: #fff; } .build-tag.auto-detect { background: var(--tag-bg); border-color: var(--border-secondary); color: var(--text-secondary); } /* Main tabs */ .main-tabs { display: flex; gap: 0; margin-bottom: 1.5rem; border-bottom: 1px solid var(--border-primary); } .main-tab { background: none; border: none; border-bottom: 2px solid transparent; color: var(--text-muted); font-family: inherit; font-size: 1rem; padding: 0.75rem 1.5rem; cursor: pointer; transition: all 0.2s; margin-bottom: -1px; } .main-tab:hover { color: var(--text-secondary); } .main-tab.active { color: var(--accent); border-bottom-color: var(--accent); } .tab-content { display: none; } .tab-content.active { display: block; } /* Story button on build cards */ .story-btn { display: inline-block; background: linear-gradient(135deg, #2a2920 0%, #1f1e18 100%); color: #c0b890 !important; padding: 0.3rem 0.75rem; border-radius: 4px; text-decoration: none !important; font-weight: bold; font-size: 0.8rem; margin-left: 0.5rem; cursor: pointer; border: 1px solid #3a3930; font-family: inherit; transition: all 0.2s; box-shadow: 1px 1px 3px rgba(0,0,0,0.3); } .story-btn:hover { transform: translateY(-1px); box-shadow: 2px 2px 6px rgba(0,0,0,0.4); background: linear-gradient(135deg, #353428 0%, #2a291f 100%); } .story-btn.has-story { background: linear-gradient(135deg, #2a2920 0%, #1f1e18 100%); border-left: 3px solid var(--accent); color: #d4c8a0 !important; } .story-btn.add-story { padding: 0.3rem 0.5rem; font-size: 1rem; line-height: 1; min-width: 1.5rem; } /* Small inline + button in build card header */ .story-btn.add-story-inline { padding: 0.15rem 0.4rem; font-size: 0.9rem; line-height: 1; min-width: auto; background: transparent; border: 1px dashed var(--text-muted); color: var(--text-muted) !important; border-radius: 3px; margin-left: 0.5rem; } .story-btn.add-story-inline:hover { border-color: var(--accent); color: var(--accent) !important; background: rgba(255, 170, 0, 0.1); } @media (prefers-color-scheme: light) { .story-btn { background: linear-gradient(135deg, #fffef0 0%, #f5f4e8 100%); color: #555 !important; border: 1px solid #d0cfc0; box-shadow: 1px 1px 3px rgba(0,0,0,0.1); } .story-btn:hover { box-shadow: 2px 2px 6px rgba(0,0,0,0.15); background: linear-gradient(135deg, #fffff8 0%, #faf9ed 100%); } .story-btn.has-story { background: linear-gradient(135deg, #fefef5 0%, #f8f7e5 100%); color: #333 !important; } } /* Story cards */ .story-card { padding: 1.5rem; background: linear-gradient(to bottom, #2d2820 0%, #252015 100%); border-radius: 4px; margin-bottom: 1rem; border: 2px solid #4a4030; border-left: 4px solid #804040; cursor: pointer; transition: all 0.2s; } .story-card:hover { border-color: #5a5040; transform: translateX(4px); } @media (prefers-color-scheme: light) { .story-card { background: linear-gradient(to bottom, #fdfbf5 0%, #f8f4e8 100%); border-color: #d4c4a0; border-left-color: #c85050; } .story-card:hover { border-color: #c4b490; } } /* Container for stories nested under builds */ .build-stories-container { margin-left: 2rem; padding-left: 1rem; border-left: 2px dashed rgba(128, 64, 64, 0.3); margin-top: -0.5rem; margin-bottom: 1rem; padding-top: 0.75rem; padding-bottom: 0.5rem; } .build-stories-container .story-feed-card { margin-bottom: 0.75rem; } .build-stories-container .story-btn.add-story { display: inline-block; padding: 0.4rem 0.75rem; margin-top: 0.5rem; font-size: 0.8rem; border: 1px dashed rgba(128, 64, 64, 0.4); background: transparent; color: var(--text-muted); cursor: pointer; border-radius: 3px; transition: all 0.2s; } .build-stories-container .story-btn.add-story:hover { border-color: var(--accent); color: var(--accent); background: rgba(128, 64, 64, 0.1); } @media (prefers-color-scheme: light) { .build-stories-container { border-left-color: rgba(200, 80, 80, 0.25); } .build-stories-container .story-btn.add-story { border-color: rgba(200, 80, 80, 0.3); } .build-stories-container .story-btn.add-story:hover { background: rgba(200, 80, 80, 0.1); } } /* Story cards in unified feed - dark theme default */ .story-feed-card { cursor: default; background: linear-gradient(to bottom, #2d2820 0%, #252015 100%); border: 2px solid #4a4030; border-left: 4px solid #804040; position: relative; padding: 1rem 1.25rem 1.25rem 1.25rem; line-height: 1.5; } .story-feed-card::before { content: ''; position: absolute; left: 28px; top: 0; bottom: 0; width: 1px; background: rgba(160, 80, 80, 0.15); } .story-feed-card:hover { border-color: #5a5040; } .story-feed-card .build-header { color: #d4c4a4; line-height: 1.6; margin-bottom: 0.5rem; } .story-feed-card .meta { color: #b4a484; line-height: 1.5; margin-top: 0.5rem; } .story-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 0.75rem; gap: 1rem; } .story-stats { color: #b4a484; font-size: 0.85rem; opacity: 0.8; } .story-feed-card .story-summary { font-family: Arial, Helvetica, sans-serif !important; color: #c4b494; line-height: 1.4; background: transparent; padding: 0; border: none; margin: 0; border-radius: 0; } .story-items-container { margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.4rem; } .story-items-preview { font-family: Arial, Helvetica, sans-serif; font-size: 0.85rem; color: #a89878; padding: 0.4rem 0.6rem; background: rgba(0,0,0,0.15); border-radius: 3px; border-left: 2px solid; line-height: 1.4; } .story-items-preview.fruit { border-left-color: var(--fruit-color); } .story-items-preview.side { border-left-color: var(--side-color); } .story-items-preview.big { border-left-color: var(--big-color); } .story-time { margin-top: 0.5rem; font-size: 0.85rem; opacity: 0.7; } @media (prefers-color-scheme: light) { .story-items-preview { background: rgba(0,0,0,0.05); color: #5a4a3a; } } @media (prefers-color-scheme: light) { .story-feed-card { background: linear-gradient(to bottom, #fdfbf5 0%, #f8f4e8 100%); border-color: #d4c4a0; border-left-color: #c85050; } .story-feed-card::before { background: rgba(200, 80, 80, 0.2); } .story-feed-card:hover { border-color: #c4b490; } .story-feed-card .build-header { color: #4a3a2a; } .story-feed-card .meta { color: #5a4a3a; } .story-feed-card .story-summary { color: #5a4a3a; } .story-stats { color: #5a4a3a; } } .platform-tag.platform-story { background: linear-gradient(135deg, #4a3d2a 0%, #3d3220 100%); color: #e8d4b8; border: 1px solid #6a5a40; } @media (prefers-color-scheme: light) { .platform-tag.platform-story { background: linear-gradient(135deg, #f5e6d3 0%, #e8d4b8 100%); color: #5a4030; border-color: #c4a882; } } .story-view-link { background: transparent; color: var(--accent) !important; text-decoration: none; font-weight: 500; padding: 0.2rem 0; border-radius: 0; font-size: 0.9rem; border-bottom: 1px dashed var(--accent); transition: all 0.2s; } .story-view-link:hover { text-decoration: none; border-bottom-style: solid; color: #fff !important; } @media (prefers-color-scheme: light) { .story-view-link:hover { color: #000 !important; } } /* Log modal */ .log-modal-content { max-width: 900px; max-height: 85vh; } .log-content { background: #1a1a1a; color: #c0c0c0; font-family: var(--font-mono); font-size: 0.8rem; padding: 1rem; border-radius: 4px; overflow: auto; max-height: 60vh; white-space: pre-wrap; word-break: break-word; line-height: 1.4; } .log-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; } .log-header a { color: var(--text-muted); font-size: 0.85rem; } .log-link { margin-left: 0.5rem; color: var(--text-muted); font-size: 0.9rem; text-decoration: none; } .log-link:hover { color: var(--accent); } .story-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; flex-wrap: wrap; gap: 0.5rem; } .story-card-title { color: var(--accent); font-weight: bold; } .story-card-date { color: var(--text-muted); font-size: 0.85rem; } .story-card-summary { color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 0.75rem; } .story-card-stats { display: flex; gap: 1rem; font-size: 0.8rem; flex-wrap: wrap; } .stat { color: var(--text-muted); } .stat-fruit { color: var(--fruit-color); } .stat-side { color: var(--side-color); } .stat-big { color: var(--big-color); } /* Story detail view */ .story-view { background: linear-gradient(to bottom, #2d2820 0%, #252015 100%); border-radius: 8px; padding: 2rem; border: 2px solid #4a4030; border-left: 4px solid #804040; } @media (prefers-color-scheme: light) { .story-view { background: linear-gradient(to bottom, #fdfbf5 0%, #f8f4e8 100%); border-color: #d4c4a0; border-left-color: #c85050; } } .story-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border-primary); flex-wrap: wrap; gap: 1rem; } .story-header h2 { color: var(--accent); font-size: 1.3rem; } .story-header-buttons { display: flex; gap: 0.5rem; flex-wrap: wrap; } .copy-llm-btn { background: linear-gradient(135deg, #2a2920 0%, #1f1e18 100%); border: 1px solid #3a3930; color: #c0b890; padding: 0.5rem 1rem; cursor: pointer; font-family: inherit; border-radius: 4px; font-size: 0.85rem; transition: all 0.2s; } .copy-llm-btn:hover { background: linear-gradient(135deg, #353428 0%, #2a291f 100%); } .copy-llm-btn.copied { background: #1a3a1a; border-color: #2a5a2a; color: #8f8; } .copy-llm-feed { padding: 0.25rem 0.5rem; font-size: 0.75rem; } @media (prefers-color-scheme: light) { .copy-llm-btn { background: linear-gradient(135deg, #f5f5f0 0%, #eaeae5 100%); border: 1px solid #ccc; color: #555; } .copy-llm-btn:hover { background: linear-gradient(135deg, #fff 0%, #f5f5f0 100%); } .copy-llm-btn.copied { background: #e5f5e5; border-color: #afa; color: #060; } } .back-btn { background: var(--bg-button); border: 1px solid var(--border-secondary); color: var(--text-secondary); padding: 0.5rem 1rem; cursor: pointer; font-family: inherit; border-radius: 4px; } .back-btn:hover { background: var(--bg-button-hover); } .story-meta { display: flex; gap: 2rem; margin-bottom: 2rem; color: var(--text-secondary); font-size: 0.9rem; flex-wrap: wrap; font-family: Arial, Helvetica, sans-serif; } .story-meta span { display: flex; align-items: center; gap: 0.3rem; } .story-summary { background: rgba(0,0,0,0.15); padding: 1rem; border-radius: 4px; margin-bottom: 2rem; border-left: 3px solid var(--accent); font-family: Arial, Helvetica, sans-serif; } @media (prefers-color-scheme: light) { .story-summary { background: rgba(0,0,0,0.05); } } .story-section { margin-bottom: 2rem; } .story-section h3 { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem; font-size: 1.1rem; font-family: Arial, Helvetica, sans-serif; } .section-fruit h3 { color: var(--fruit-color); } .section-side h3 { color: var(--side-color); } .section-big h3 { color: var(--big-color); } .story-items { list-style: none; } .story-items li { padding: 0.75rem 1rem; margin-bottom: 0.5rem; background: rgba(0,0,0,0.15); border-radius: 4px; border-left: 3px solid var(--border-primary); display: flex; align-items: flex-start; gap: 0.75rem; } @media (prefers-color-scheme: light) { .story-items li { background: rgba(0,0,0,0.05); } } .section-fruit .story-items li { border-left-color: var(--fruit-color); } .section-side .story-items li { border-left-color: var(--side-color); } .section-big .story-items li { border-left-color: var(--big-color); } .item-icon { font-size: 1.1rem; flex-shrink: 0; } .item-content { flex: 1; font-family: Arial, Helvetica, sans-serif; } .item-title { font-weight: bold; margin-bottom: 0.25rem; } .item-desc { color: var(--text-secondary); font-size: 0.9rem; line-height: 1.5; } /* Modal */ .modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.85); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 1rem; } .modal-content { background: var(--bg-card); border: 1px solid var(--border-primary); border-radius: 8px; width: 100%; max-width: 600px; max-height: 90vh; overflow-y: auto; padding: 1.5rem; } .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border-primary); } .modal-header h2 { color: var(--accent); font-size: 1.2rem; } .close-btn { background: none; border: none; color: var(--text-muted); font-size: 1.5rem; cursor: pointer; padding: 0; line-height: 1; } .close-btn:hover { color: var(--text-primary); } .form-row { margin-bottom: 1rem; } .form-row label { display: block; color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 0.25rem; } .form-row input, .form-row textarea, .form-row select { width: 100%; background: var(--bg-input); border: 1px solid var(--border-input); color: var(--text-primary); font-family: inherit; font-size: 0.95rem; padding: 0.6rem; border-radius: 4px; } .form-row input:focus, .form-row textarea:focus, .form-row select:focus { outline: none; border-color: var(--accent); } .form-section { margin: 1.5rem 0; padding: 1rem; background: var(--bg-secondary); border-radius: 6px; } .form-section h3 { font-size: 1rem; margin-bottom: 0.25rem; } .form-hint { color: var(--text-muted); font-size: 0.8rem; margin-bottom: 0.75rem; } .item-list { margin-bottom: 0.5rem; } .item-row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; align-items: center; } .item-row .item-title-input { flex: 1; background: var(--bg-input); border: 1px solid var(--border-input); color: var(--text-primary); font-family: inherit; padding: 0.5rem; border-radius: 4px; font-size: 0.9rem; } .item-row .item-desc-input { flex: 1.5; background: var(--bg-input); border: 1px solid var(--border-input); color: var(--text-primary); font-family: inherit; padding: 0.5rem; border-radius: 4px; font-size: 0.9rem; } .item-row input:focus { outline: none; border-color: var(--accent); } .remove-item-btn { background: none; border: none; color: var(--error); cursor: pointer; font-size: 1rem; padding: 0.25rem; opacity: 0.6; } .remove-item-btn:hover { opacity: 1; } .add-item-btn { background: none; border: 1px dashed var(--border-secondary); color: var(--text-muted); padding: 0.4rem 0.75rem; cursor: pointer; font-family: inherit; font-size: 0.85rem; border-radius: 4px; } .add-item-btn:hover { border-color: var(--accent); color: var(--accent); } .form-actions { display: flex; gap: 1rem; justify-content: flex-end; margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border-primary); } .cancel-btn { background: var(--bg-button); border: 1px solid var(--border-secondary); color: var(--text-secondary); padding: 0.6rem 1.25rem; cursor: pointer; font-family: inherit; border-radius: 4px; } .cancel-btn:hover { background: var(--bg-button-hover); } .submit-btn { background: linear-gradient(135deg, var(--accent) 0%, #f60 100%); border: none; color: #000; padding: 0.6rem 1.5rem; cursor: pointer; font-family: inherit; font-weight: bold; border-radius: 4px; } .submit-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 8px rgba(255, 170, 0, 0.4); } .no-stories { color: var(--text-muted); font-style: italic; padding: 2rem; text-align: center; }
/* Mobile-friendly styles */ @media (max-width: 768px) { body { padding: 1rem; font-size: 15px; -webkit-text-size-adjust: 100%; } h1 { font-size: 1.25rem; } h2 { font-size: 1.1rem; } .container { padding: 0; }
/* Platform filter tabs - wrap on mobile */ .platform-filters { gap: 0.4rem; margin-bottom: 1rem; flex-wrap: wrap; } .platform-filters button { padding: 0.4rem 0.6rem; font-size: 0.8rem; }
/* Build cards */ .build-card { padding: 1rem; margin-bottom: 1rem; } .build-header { flex-wrap: wrap; gap: 0.5rem; } .platform-tag { font-size: 0.7rem; padding: 0.15rem 0.4rem; } .project-name { font-size: 0.9rem; } .build-tags { width: 100%; margin-top: 0.25rem; } .meta { font-size: 0.8rem; }
/* Story cards */ .story-feed-card { padding: 0.875rem 1rem; } .story-feed-card .build-header { gap: 0.4rem; } .story-feed-card .project-name { font-size: 0.85rem; } .story-view-link { font-size: 0.8rem; } .copy-llm-btn.copy-llm-feed { font-size: 0.7rem; padding: 0.2rem 0.4rem; } .story-summary { font-size: 0.85rem; } .story-time { font-size: 0.75rem; }
/* Nested stories container */ .build-stories-container { margin-left: 1rem; padding-left: 0.75rem; }
/* Modals */ .modal-content { padding: 1.25rem; margin: 0.5rem; max-height: calc(100vh - 1rem); } .modal-content h2 { font-size: 1.1rem; } .form-group label { font-size: 0.85rem; } .form-group input, .form-group textarea, .form-group select { font-size: 16px; /* Prevents iOS zoom on focus */ padding: 0.6rem; } .modal-actions { flex-direction: column; gap: 0.5rem; } .modal-actions button { width: 100%; }
/* Story detail view */ .story-meta { flex-direction: column; gap: 0.5rem; font-size: 0.85rem; } .story-section h3 { font-size: 1rem; } .story-items li { font-size: 0.9rem; } .item-title { font-size: 0.9rem; } .item-desc { font-size: 0.8rem; }
/* Date headers */ .date-header { font-size: 0.9rem; padding: 0.5rem 0; }
/* Footer */ .footer { font-size: 0.75rem; padding: 1rem 0; }
/* iOS install button - larger touch target */ .ios-install-btn { padding: 0.5rem 0.75rem; font-size: 0.85rem; } }
/* Extra small screens (phones in portrait) */ @media (max-width: 480px) { body { padding: 0.75rem; } .platform-filters button { padding: 0.4rem 0.6rem; font-size: 0.8rem; } .build-card { padding: 0.875rem; } .story-feed-card { padding: 0.75rem; } .build-stories-container { margin-left: 0.5rem; padding-left: 0.5rem; } .copy-llm-btn.copy-llm-feed { display: none; /* Hide on very small screens */ } }
/* Touch-friendly tap targets */ @media (pointer: coarse) { .platform-filters button { padding: 0.6rem 0.9rem; } .story-btn, .ios-install-btn, .log-link { padding: 0.5rem 0.75rem; } }
/* Improve contrast for outdoor/bright viewing */ @media (prefers-color-scheme: light) { .meta, .story-time { color: #444; } .text-muted, .story-card-date { color: #666; } } </style></head><body> <div id="auth-container"> <img class="logo" src="https://assets.aesthetic.computer/false.work/fw-logo-2.svg" alt="false.work" style="width: 400px; margin-top: 4rem; margin-bottom: 2rem;"> <input type="password" id="password" placeholder="Enter password..." /> <button onclick="checkPassword()">Access Builds</button> <div id="error" class="error"></div> </div>
<div id="content"> <div class="container"> <div style="display: flex; justify-content: center; align-items: center; padding-top: 2rem; margin-bottom: 2rem;"> <img class="logo" src="https://assets.aesthetic.computer/false.work/fw-logo-2.svg" alt="false.work" style="width: 400px;"> </div>
<!-- Single content view with platform filters including Stories --> <div id="main-content"> <div class="platform-filters"> <button data-platform="all" class="active" onclick="filterPlatform('all')">✨ New</button> <button data-platform="stories" onclick="filterPlatform('stories')">📝 Stories</button> <button data-platform="windows" onclick="filterPlatform('windows')">🪟 Windows</button> <button data-platform="mac" onclick="filterPlatform('mac')">🍎 Mac</button> <button data-platform="ios" onclick="filterPlatform('ios')">📱 iOS</button> <button data-platform="plugins" onclick="filterPlatform('plugins')">🔌 Plugins</button> <button class="disabled" title="Coming soon">🎮 Gameboy</button> <button class="disabled" title="Coming soon">🟡 Playdate</button> <button class="disabled" title="Coming soon">🕹️ N64</button> </div>
<!-- Builds list (shown for all/windows/mac/ios) --> <div id="builds-list"> <div class="loading">Loading builds</div> </div>
<!-- Stories list (shown for stories filter) --> <div id="stories-list" style="display: none;"> <div id="stories-content"> <div class="loading">Loading stories</div> </div> </div>
<!-- Plugins list (shown for plugins filter) --> <div id="plugins-list" style="display: none;"> <div id="plugins-content"> <div class="loading">Loading plugins</div> </div> </div> </div>
<!-- STORY DETAIL VIEW --> <div id="story-view" style="display: none;"> <div class="story-view"> <div class="story-header"> <h2>📝 Story</h2> <div class="story-header-buttons"> <button class="copy-llm-btn" onclick="copyStoryForLLM()">📋 Copy for LLM</button> <button class="back-btn" onclick="hideStoryDetail()">← Back to Stories</button> </div> </div> <div id="story-detail-content"></div> </div> </div>
<footer> <span id="footer-lock">🔒</span> <a href="https://builds.false.work">builds.false.work</a> <span id="logout-link" style="display: none;"> · <a href="#" onclick="logout(); return false;">logout</a> · <a href="roadmap.html">🗺️ roadmap</a></span> </footer> </div> </div>
<!-- STORY FORM MODAL --> <div id="story-modal" class="modal" style="display: none;"> <div class="modal-content"> <div class="modal-header"> <h2>📝 Write a Story</h2> <button class="close-btn" onclick="closeStoryModal()">✕</button> </div>
<form id="story-form" onsubmit="submitStory(event)"> <div class="form-row"> <label>Build</label> <select id="form-build" required> <option value="">Select a build...</option> </select> </div>
<div class="form-row"> <label>Your Name</label> <input type="text" id="form-tester" placeholder="Who played this build?" required> </div>
<div class="form-row"> <label>Summary</label> <textarea id="form-summary" rows="2" placeholder="What was your experience like?" required></textarea> </div>
<div class="form-section"> <h3 style="color: var(--fruit-color);">🍎 Low Hanging Fruit</h3> <p class="form-hint">Quick fixes, obvious bugs, easy wins</p> <div id="fruit-items" class="item-list"></div> <button type="button" class="add-item-btn" onclick="addItem('fruit-items')">+ Add item</button> </div>
<div class="form-section"> <h3 style="color: var(--side-color);">🍟 A Little on the Side</h3> <p class="form-hint">Nice-to-haves, polish suggestions, extra ideas</p> <div id="side-items" class="item-list"></div> <button type="button" class="add-item-btn" onclick="addItem('side-items')">+ Add item</button> </div>
<div class="form-section"> <h3 style="color: var(--big-color);">💡 Bigger Ideas</h3> <p class="form-hint">Deeper considerations, design thoughts, major issues</p> <div id="big-items" class="item-list"></div> <button type="button" class="add-item-btn" onclick="addItem('big-items')">+ Add item</button> </div>
<div class="form-actions"> <button type="button" class="cancel-btn" onclick="closeStoryModal()">Cancel</button> <button type="submit" class="submit-btn">Submit Story</button> </div> </form> </div> </div>
<!-- LOG MODAL --> <div id="log-modal" class="modal" style="display: none;"> <div class="modal-content log-modal-content"> <div class="log-header"> <h2 style="color: var(--accent); font-size: 1.1rem;">📜 Build Log</h2> <div> <a id="log-download-link" href="#" target="_blank">Download .txt ↗</a> <button class="close-btn" onclick="closeLogModal()" style="margin-left: 1rem;">✕</button> </div> </div> <div id="log-content" class="log-content">Loading...</div> </div> </div>
<script> let sessionPassword = localStorage.getItem('builds-session') || null; let allBuilds = []; let allStories = []; let allPlugins = []; let currentFilter = 'all'; let currentTab = 'builds';
// Auto-login on page load if session exists (async function init() { if (sessionPassword) { try { const res = await fetch('/.netlify/functions/verify-builds-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: sessionPassword }) }); const data = await res.json(); if (data.success) { showAuthenticatedUI(); await Promise.all([loadBuilds(), loadStories(), loadPlugins()]); handleRoute(); return; } } catch (err) {} // Session invalid, clear it localStorage.removeItem('builds-session'); sessionPassword = null; } })();
function showAuthenticatedUI() { document.getElementById('auth-container').style.display = 'none'; document.body.classList.add('authenticated'); document.getElementById('footer-lock').textContent = '🔓'; document.getElementById('logout-link').style.display = 'inline'; }
async function checkPassword() { const password = document.getElementById('password').value;
try { const res = await fetch('/.netlify/functions/verify-builds-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) }); const data = await res.json();
if (data.success) { sessionPassword = password; localStorage.setItem('builds-session', password); showAuthenticatedUI(); await Promise.all([loadBuilds(), loadStories(), loadPlugins()]); handleRoute(); } else { document.getElementById('error').textContent = 'Incorrect password'; document.getElementById('password').value = ''; } } catch (err) { document.getElementById('error').textContent = 'Authentication error'; } }
// View management function showStoryDetail(updateHash = true) { document.getElementById('main-content').style.display = 'none'; document.getElementById('story-view').style.display = 'block'; if (updateHash && currentViewingStoryId) { history.pushState(null, '', `#story/${currentViewingStoryId}`); } }
function hideStoryDetail() { document.getElementById('story-view').style.display = 'none'; document.getElementById('main-content').style.display = 'block'; filterPlatform('stories'); }
// Handle URL routing function handleRoute() { const hash = window.location.hash.slice(1); // remove #
// Handle story detail view if (hash.startsWith('story/')) { const storyId = hash.slice(6); const story = allStories.find(r => r._id === storyId); if (story) { viewStory(storyId); return; } }
// Handle platform filters const platformMap = { '': 'all', 'stories': 'stories', 'windows': 'windows', 'mac': 'mac', 'ios': 'ios', 'plugins': 'plugins' };
const platform = platformMap[hash] || 'all'; filterPlatform(platform, false); }
window.addEventListener('popstate', handleRoute);
// === BUILDS === async function loadBuilds() { const container = document.getElementById('builds-list'); container.innerHTML = '<div class="loading">Loading builds</div>';
try { const res = await fetch('/.netlify/functions/get-builds', { headers: { 'Authorization': `Bearer ${sessionPassword}` } });
if (!res.ok) throw new Error('Failed to load builds');
const data = await res.json(); allBuilds = data.builds || []; renderBuilds(); populateBuildSelector(); } catch (err) { container.innerHTML = '<div class="error">Failed to load builds. Please try again.</div>'; console.error(err); } }
// === PLUGINS === async function loadPlugins() { const container = document.getElementById('plugins-content'); if (!container) return;
try { const res = await fetch('/.netlify/functions/get-plugins', { headers: { 'Authorization': `Bearer ${sessionPassword}` } });
if (!res.ok) throw new Error('Failed to load plugins');
const data = await res.json(); allPlugins = data.plugins || []; } catch (err) { console.error('loadPlugins:', err); allPlugins = []; } }
function renderPlugins() { const container = document.getElementById('plugins-content'); if (!container) return;
if (allPlugins.length === 0) { container.innerHTML = '<p style="color: var(--text-muted); font-style: italic;">No plugins registered yet. Run <code>fish false.work/unreal-builder/scripts/register-serialcom-plugin.fish</code> after deploying to seed the first entry.</p>'; return; }
const artifactEmoji = { 'source': '📄', 'packaged-win64': '🪟', 'packaged-mac': '🍎', 'packaged-linux': '🐧', 'test-project': '📦', 'patch': '🩹' };
let html = ''; allPlugins.forEach(plugin => { html += '<div class="plugin-card">'; html += '<div class="plugin-header">'; html += `<span class="platform-tag platform-plugin">🔌 ${plugin.displayName || plugin.name}</span>`; html += `<span class="plugin-version">v${plugin.version}</span>`; if (plugin.engineVersion) { html += `<span class="plugin-engine-tag">UE ${plugin.engineVersion}</span>`; } if (plugin.platforms && plugin.platforms.length) { html += `<span class="plugin-engine-tag">${plugin.platforms.join(', ')}</span>`; } html += '</div>';
if (plugin.description) { html += `<div class="plugin-description">${escapeHtml(plugin.description)}</div>`; }
if (plugin.artifacts && plugin.artifacts.length) { html += '<div class="plugin-artifacts">'; plugin.artifacts.forEach(a => { const emoji = artifactEmoji[a.kind] || '📦'; const size = typeof a.sizeMB === 'number' ? `<span class="size">${a.sizeMB} MB</span>` : ''; html += `<a href="${a.downloadUrl}" class="plugin-artifact-btn" title="${escapeHtml(a.kind)}">${emoji} ${escapeHtml(a.label)} ${size}</a>`; }); html += '</div>'; }
if (plugin.upstream) { html += `<div class="plugin-upstream">Upstream: <a href="${plugin.upstream}" target="_blank" rel="noopener">${escapeHtml(plugin.upstream)}</a></div>`; }
html += '</div>'; });
container.innerHTML = html; }
function escapeHtml(str) { if (str === undefined || str === null) return ''; return String(str) .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }
function filterPlatform(platform, updateHash = true) { currentFilter = platform; document.querySelectorAll('.platform-filters button').forEach(btn => { btn.classList.toggle('active', btn.dataset.platform === platform); });
// Show/hide builds vs stories vs plugins const buildsList = document.getElementById('builds-list'); const storiesList = document.getElementById('stories-list'); const pluginsList = document.getElementById('plugins-list');
if (platform === 'stories') { buildsList.style.display = 'none'; storiesList.style.display = 'block'; if (pluginsList) pluginsList.style.display = 'none'; renderStories(); } else if (platform === 'plugins') { buildsList.style.display = 'none'; storiesList.style.display = 'none'; if (pluginsList) pluginsList.style.display = 'block'; renderPlugins(); } else { buildsList.style.display = 'block'; storiesList.style.display = 'none'; if (pluginsList) pluginsList.style.display = 'none'; renderBuilds(); }
if (updateHash) { const hashMap = { all: '', stories: 'stories', windows: 'windows', mac: 'mac', ios: 'ios', plugins: 'plugins' }; history.pushState(null, '', platform === 'all' ? '#' : `#${hashMap[platform]}`); } }
function renderBuilds() { const container = document.getElementById('builds-list'); const filtered = currentFilter === 'all' ? allBuilds : allBuilds.filter(b => b.platform === currentFilter);
if (filtered.length === 0 && (currentFilter !== 'all' || allStories.length === 0)) { container.innerHTML = '<p style="color: #666; font-style: italic;">No builds available.</p>'; return; }
const grouped = {}; const today = new Date().toDateString(); const yesterday = new Date(Date.now() - 86400000).toDateString();
function getDateLabel(date) { const dateStr = date.toDateString(); if (dateStr === today) return 'Today'; if (dateStr === yesterday) return 'Yesterday'; return date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); }
// Add builds to grouped (stories will be nested under builds) filtered.forEach(build => { const label = getDateLabel(new Date(build.timestamp)); if (!grouped[label]) grouped[label] = []; // Find stories for this build const buildStories = currentFilter === 'all' ? allStories.filter(s => s.buildPlatform === build.platform && s.buildVersion === build.version) : []; grouped[label].push({ type: 'build', data: build, stories: buildStories, timestamp: new Date(build.timestamp) }); });
// Add orphan stories (stories without a matching build in view) if showing "all" if (currentFilter === 'all') { const buildKeys = new Set(filtered.map(b => `${b.platform}:${b.version}`)); allStories.forEach(story => { const key = `${story.buildPlatform}:${story.buildVersion}`; if (!buildKeys.has(key)) { // Orphan story - no matching build const label = getDateLabel(new Date(story.createdAt)); if (!grouped[label]) grouped[label] = []; grouped[label].push({ type: 'orphan-story', data: story, timestamp: new Date(story.createdAt) }); } }); }
// Sort groups by date (most recent first) const sortedLabels = Object.keys(grouped).sort((a, b) => { if (a === 'Today') return -1; if (b === 'Today') return 1; if (a === 'Yesterday') return -1; if (b === 'Yesterday') return 1; return new Date(b) - new Date(a); });
let html = ''; sortedLabels.forEach(dateLabel => { const isToday = dateLabel === 'Today'; html += `<h3 class="date-header ${isToday ? 'today' : 'previous'}">${dateLabel}</h3>`;
// Sort items within each day by timestamp descending grouped[dateLabel].sort((a, b) => b.timestamp - a.timestamp);
grouped[dateLabel].forEach(item => { if (item.type === 'build') { const build = item.data; const isFailed = build.status === 'failed' || build.status === 'stale'; const hasStories = item.stories && item.stories.length > 0; html += renderBuildCard(build, hasStories); // Render nested stories under build (only if there are stories) if (item.stories && item.stories.length > 0) { html += `<div class="build-stories-container">`; // Sort stories by date descending item.stories.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); item.stories.forEach(story => { html += renderStoryCardInFeed(story); }); // Add + button at end of stories container if (!isFailed) { html += `<button class="story-btn add-story" onclick="openStoryModalForBuild('${build.platform}', '${build.version}')" title="Write a story for this build">+ Add Story</button>`; } html += `</div>`; } } else if (item.type === 'orphan-story') { html += renderStoryCardInFeed(item.data); } }); });
container.innerHTML = html; updateBuildTimes(); }
function renderStoryCardInFeed(story) { // Delegate to shared renderStoryCard function return renderStoryCard(story); }
function renderBuildCard(build, hasStories = false) { const platformEmoji = { windows: '🪟', mac: '🍎', ios: '📱' }; const platformLabel = { windows: 'Windows', mac: 'Mac', ios: 'iOS' }; const fileExt = build.platform === 'ios' ? 'ipa' : 'zip'; const isFailed = build.status === 'failed' || build.status === 'stale';
let html = ` <div class="build-card ${build.platform}${isFailed ? ' failed' : ''}"> <div class="build-header"> <span class="platform-tag platform-${build.platform}">${platformEmoji[build.platform]} ${platformLabel[build.platform]}</span> <span class="project-name">SpiderLily</span> <a href="${build.downloadUrl}">${build.version}.${fileExt}</a> `;
if (build.status && build.status !== 'ok') { html += `<span class="status-badge ${build.status}">${build.status}</span>`; }
if (build.logUrl) { html += `<a href="${build.logUrl}" class="log-link" onclick="event.preventDefault(); openLogModal('${build.logUrl}')" title="Right-click to download">(log)</a>`; }
// Small inline + button when no stories exist yet if (!isFailed && !hasStories) { html += `<button class="story-btn add-story-inline" onclick="openStoryModalForBuild('${build.platform}', '${build.version}')" title="Write a story">+</button>`; }
if (build.platform === 'ios' && !isFailed) { html += `<a href="itms-services://?action=download-manifest&url=https://builds.false.work/ios/manifest.plist" class="ios-install-btn">📲 Install</a>`; }
html += `</div>`;
const parts = []; parts.push(`${build.sizeMB} MB`); if (build.changelist) parts.push(build.changelist); else if (build.level) parts.push(build.level); if (build.ueVersion) parts.push(build.ueVersion);
html += `<div class="meta">${parts.join(' | ')} | <span class="build-time" data-date="${build.timestamp}">...</span></div>`;
if (build.statusReason) { html += `<div class="status-reason">⚠️ ${build.statusReason}</div>`; }
if (build.platform === 'ios' && !isFailed) { html += `<div class="ios-note">💡 Open in Safari on your iOS device and tap "Install" for wireless installation.</div>`; }
html += `</div>`; return html; }
function updateBuildTimes() { const dayColors = ['#f66', '#fa0', '#ff0', '#0f0', '#0ff', '#09f', '#f0f'];
document.querySelectorAll('.build-time').forEach(el => { const buildDate = new Date(el.dataset.date); const now = new Date(); const diffMs = now - buildDate; const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffDays = Math.floor(diffHours / 24);
el.style.color = dayColors[buildDate.getDay()];
const ptTime = buildDate.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, timeZone: 'America/Los_Angeles' }) + ' PT';
let timeAgo; if (diffHours < 1) { const diffMins = Math.floor(diffMs / (1000 * 60)); timeAgo = diffMins < 1 ? 'just now' : `${diffMins}m ago`; } else if (diffHours < 24) { timeAgo = `${diffHours}h ago`; } else if (diffDays < 7) { timeAgo = `${diffDays}d ago`; } else { timeAgo = buildDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); }
el.textContent = `${timeAgo} (${ptTime})`; }); }
// === STORIES === async function loadStories() { try { const res = await fetch('/.netlify/functions/stories', { headers: { 'Authorization': `Bearer ${sessionPassword}` } });
if (!res.ok) { // Stories endpoint may not exist yet allStories = []; return; }
const data = await res.json(); allStories = (data.stories || []).filter(r => !r.deleted); // Only render if stories view is currently active if (currentFilter === 'stories') { renderStories(); } // Re-render builds to show story buttons renderBuilds(); } catch (err) { allStories = []; console.error('Stories not available yet:', err); } }
function renderStories() { const container = document.getElementById('stories-content'); if (!container) { console.error('stories-content element not found'); return; }
if (allStories.length === 0) { container.innerHTML = '<div class="no-stories">No stories yet. Play a build and write about your experience!</div>'; return; }
const platformEmoji = { windows: '🪟', mac: '🍎', ios: '📱' };
// Group stories by build (platform + version) const grouped = {}; allStories.forEach(story => { const buildKey = `${story.buildPlatform}|${story.buildVersion}`; if (!grouped[buildKey]) { grouped[buildKey] = { platform: story.buildPlatform, version: story.buildVersion, stories: [] }; } grouped[buildKey].stories.push(story); });
// Sort builds by newest story first, then render const sortedBuilds = Object.values(grouped).sort((a, b) => { const aLatest = Math.max(...a.stories.map(r => new Date(r.createdAt).getTime())); const bLatest = Math.max(...b.stories.map(r => new Date(r.createdAt).getTime())); return bLatest - aLatest; });
let html = ''; sortedBuilds.forEach(build => { const emoji = platformEmoji[build.platform] || '📦'; const storyCount = build.stories.length; html += `<h3 class="date-header">${emoji} SpiderLily ${build.version} <span style="font-weight: normal; color: var(--text-muted);">(${storyCount} stor${storyCount !== 1 ? 'ies' : 'y'})</span></h3>`;
// Sort stories within build by date descending build.stories.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); build.stories.forEach(story => { html += renderStoryCard(story); }); });
container.innerHTML = html; }
function renderStoryCard(story) { const platformEmoji = { windows: '🪟', mac: '🍎', ios: '📱' }; const fruitItems = story.lowHangingFruit || []; const sideItems = story.onTheSide || []; const bigItems = story.biggerThoughts || [];
const time = new Date(story.createdAt).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, timeZone: 'America/Los_Angeles' }) + ' PT';
const tester = story.tester || 'Anonymous';
return ` <div class="build-card story-feed-card" data-story-id="${story._id}"> <div class="build-header"> <span class="platform-tag platform-story">📝 Story</span> <span class="project-name">SpiderLily ${story.buildVersion}</span> <button class="copy-llm-btn copy-llm-feed" onclick="copyStoryForLLMById('${story._id}')">📋 Copy for LLM</button> <span class="build-tags"> <span class="build-tag">${platformEmoji[story.buildPlatform] || '📦'} ${story.buildPlatform}</span> </span> </div> <div class="meta" style="margin-top: 0.75rem;"> by <strong>${tester}</strong> · <span class="story-summary">"${story.summary || 'No summary'}"</span> </div> <div class="story-footer"> <span class="story-stats">🍎 ${fruitItems.length} fruit · 🍟 ${sideItems.length} side · 💡 ${bigItems.length} ideas · ${time}</span> <a href="#story/${story._id}" class="story-view-link" onclick="event.preventDefault(); viewStory('${story._id}')">Read Story →</a> </div> </div> `; }
function viewStory(storyId) { const story = allStories.find(r => r._id === storyId); if (!story) return;
const platformEmoji = { windows: '🪟', mac: '🍎', ios: '📱' }; const dateStr = new Date(story.createdAt).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
let html = ` <div class="story-meta"> <span>${platformEmoji[story.buildPlatform] || '📦'} <strong>${story.buildPlatform}</strong> ${story.buildVersion}</span> <span>👤 <strong>${story.tester || 'Anonymous'}</strong></span> <span>📅 ${dateStr}</span> </div> <div class="story-summary"><strong>Summary:</strong> ${story.summary || 'No summary provided.'}</div> `;
if (story.lowHangingFruit && story.lowHangingFruit.length > 0) { html += `<div class="story-section section-fruit"><h3>🍎 Low Hanging Fruit</h3><ul class="story-items">`; story.lowHangingFruit.forEach(item => { html += `<li><span class="item-icon">🔧</span><div class="item-content"><div class="item-title">${item.title}</div>${item.desc ? `<div class="item-desc">${item.desc}</div>` : ''}</div></li>`; }); html += `</ul></div>`; }
if (story.onTheSide && story.onTheSide.length > 0) { html += `<div class="story-section section-side"><h3>🍟 A Little on the Side</h3><ul class="story-items">`; story.onTheSide.forEach(item => { html += `<li><span class="item-icon">✨</span><div class="item-content"><div class="item-title">${item.title}</div>${item.desc ? `<div class="item-desc">${item.desc}</div>` : ''}</div></li>`; }); html += `</ul></div>`; }
if (story.biggerThoughts && story.biggerThoughts.length > 0) { html += `<div class="story-section section-big"><h3>💡 Bigger Ideas</h3><ul class="story-items">`; story.biggerThoughts.forEach(item => { html += `<li><span class="item-icon">🌟</span><div class="item-content"><div class="item-title">${item.title}</div>${item.desc ? `<div class="item-desc">${item.desc}</div>` : ''}</div></li>`; }); html += `</ul></div>`; }
document.getElementById('story-detail-content').innerHTML = html; currentViewingStoryId = storyId; showStoryDetail(); }
let currentViewingStoryId = null;
function copyStoryForLLM() { const story = allStories.find(r => r._id === currentViewingStoryId); if (!story) return;
const platformLabel = { windows: 'Windows', mac: 'Mac', ios: 'iOS' }; const dateStr = new Date(story.createdAt).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });
let text = `# SpiderLily Story\n\n`; text += `**Platform:** ${platformLabel[story.buildPlatform] || story.buildPlatform}\n`; text += `**Build:** ${story.buildVersion}\n`; text += `**Player:** ${story.tester || 'Anonymous'}\n`; text += `**Date:** ${dateStr}\n\n`; text += `## Summary\n${story.summary || 'No summary provided.'}\n\n`;
if (story.lowHangingFruit && story.lowHangingFruit.length > 0) { text += `## Low Hanging Fruit (Quick Fixes)\n`; story.lowHangingFruit.forEach((item, i) => { text += `${i + 1}. **${item.title}**`; if (item.desc) text += `\n ${item.desc}`; text += `\n`; }); text += `\n`; }
if (story.onTheSide && story.onTheSide.length > 0) { text += `## On the Side (Nice-to-Haves)\n`; story.onTheSide.forEach((item, i) => { text += `${i + 1}. **${item.title}**`; if (item.desc) text += `\n ${item.desc}`; text += `\n`; }); text += `\n`; }
if (story.biggerThoughts && story.biggerThoughts.length > 0) { text += `## Bigger Thoughts (Design/Architecture)\n`; story.biggerThoughts.forEach((item, i) => { text += `${i + 1}. **${item.title}**`; if (item.desc) text += `\n ${item.desc}`; text += `\n`; }); }
navigator.clipboard.writeText(text).then(() => { const btn = document.querySelector('.copy-llm-btn'); btn.textContent = '✓ Copied!'; btn.classList.add('copied'); setTimeout(() => { btn.textContent = '📋 Copy for LLM'; btn.classList.remove('copied'); }, 2000); }); }
function copyStoryForLLMById(storyId) { const story = allStories.find(r => r._id === storyId); if (!story) return;
const platformLabel = { windows: 'Windows', mac: 'Mac', ios: 'iOS' }; const dateStr = new Date(story.createdAt).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });
let text = `# SpiderLily Story\n\n`; text += `**Platform:** ${platformLabel[story.buildPlatform] || story.buildPlatform}\n`; text += `**Build:** ${story.buildVersion}\n`; text += `**Player:** ${story.tester || 'Anonymous'}\n`; text += `**Date:** ${dateStr}\n\n`; text += `## Summary\n${story.summary || 'No summary provided.'}\n\n`;
if (story.lowHangingFruit && story.lowHangingFruit.length > 0) { text += `## Low Hanging Fruit (Quick Fixes)\n`; story.lowHangingFruit.forEach((item, i) => { text += `${i + 1}. **${item.title}**`; if (item.desc) text += `\n ${item.desc}`; text += `\n`; }); text += `\n`; }
if (story.onTheSide && story.onTheSide.length > 0) { text += `## On the Side (Nice-to-Haves)\n`; story.onTheSide.forEach((item, i) => { text += `${i + 1}. **${item.title}**`; if (item.desc) text += `\n ${item.desc}`; text += `\n`; }); text += `\n`; }
if (story.biggerThoughts && story.biggerThoughts.length > 0) { text += `## Bigger Thoughts (Design/Architecture)\n`; story.biggerThoughts.forEach((item, i) => { text += `${i + 1}. **${item.title}**`; if (item.desc) text += `\n ${item.desc}`; text += `\n`; }); }
navigator.clipboard.writeText(text).then(() => { const btn = document.querySelector(`[data-story-id="${storyId}"] .copy-llm-feed`); if (btn) { btn.textContent = '✓ Copied!'; btn.classList.add('copied'); setTimeout(() => { btn.textContent = '📋 Copy for LLM'; btn.classList.remove('copied'); }, 2000); } }); }
function viewStoryForBuild(platform, version) { const story = allStories.find(r => r.buildPlatform === platform && r.buildVersion === version); if (story) viewStory(story._id); }
// === STORY FORM === function populateBuildSelector() { const select = document.getElementById('form-build'); select.innerHTML = '<option value="">Select a build...</option>';
const platformEmoji = { windows: '🪟', mac: '🍎', ios: '📱' }; const platformLabel = { windows: 'Win', mac: 'Mac', ios: 'iOS' };
// Group builds by date const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const yesterday = new Date(today.getTime() - 86400000);
// Sort by timestamp descending (newest first) const sortedBuilds = [...allBuilds].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp) );
sortedBuilds.forEach(build => { const buildDate = new Date(build.timestamp); const buildDay = new Date(buildDate.getFullYear(), buildDate.getMonth(), buildDate.getDate());
let recency = ''; if (buildDay.getTime() === today.getTime()) { recency = 'Today'; } else if (buildDay.getTime() === yesterday.getTime()) { recency = 'Yesterday'; } else { recency = buildDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); }
const time = buildDate.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
const tags = build.tags && build.tags.length > 0 ? ` [${build.tags.map(t => t === 'auto-detect' ? 'Auto' : t).join(', ')}]` : '';
const opt = document.createElement('option'); opt.value = `${build.platform}|${build.version}`; opt.textContent = `${platformEmoji[build.platform]} ${platformLabel[build.platform]} ${build.version} — ${recency} ${time}${tags}`; select.appendChild(opt); }); }
function openStoryModal() { document.getElementById('story-modal').style.display = 'flex'; // Clear form document.getElementById('story-form').reset(); ['fruit-items', 'side-items', 'big-items'].forEach(id => { document.getElementById(id).innerHTML = ''; }); }
function openStoryModalForBuild(platform, version) { openStoryModal(); document.getElementById('form-build').value = `${platform}|${version}`; }
function closeStoryModal() { document.getElementById('story-modal').style.display = 'none'; }
// Log modal functions async function openLogModal(logUrl) { document.getElementById('log-modal').style.display = 'flex'; document.getElementById('log-download-link').href = logUrl; document.getElementById('log-content').textContent = 'Loading...';
try { const res = await fetch(logUrl); const text = await res.text(); document.getElementById('log-content').textContent = text; } catch (err) { document.getElementById('log-content').textContent = 'Failed to load log file.'; } }
function closeLogModal() { document.getElementById('log-modal').style.display = 'none'; }
function addItem(containerId) { const container = document.getElementById(containerId); const row = document.createElement('div'); row.className = 'item-row'; row.innerHTML = ` <input type="text" placeholder="Title" class="item-title-input"> <input type="text" placeholder="Description (optional)" class="item-desc-input"> <button type="button" class="remove-item-btn" onclick="this.parentElement.remove()">✕</button> `; container.appendChild(row); row.querySelector('.item-title-input').focus(); }
function collectItems(containerId) { const items = []; document.querySelectorAll(`#${containerId} .item-row`).forEach(row => { const title = row.querySelector('.item-title-input').value.trim(); const desc = row.querySelector('.item-desc-input').value.trim(); if (title) items.push({ title, desc }); }); return items; }
async function submitStory(e) { e.preventDefault();
const buildValue = document.getElementById('form-build').value; if (!buildValue) { alert('Please select a build'); return; }
const [buildPlatform, buildVersion] = buildValue.split('|');
const story = { buildPlatform, buildVersion, tester: document.getElementById('form-tester').value.trim(), summary: document.getElementById('form-summary').value.trim(), lowHangingFruit: collectItems('fruit-items'), onTheSide: collectItems('side-items'), biggerThoughts: collectItems('big-items') };
try { const res = await fetch('/.netlify/functions/stories', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${sessionPassword}` }, body: JSON.stringify(story) });
if (!res.ok) throw new Error('Failed to save story');
closeStoryModal(); await loadStories(); filterPlatform('stories'); } catch (err) { console.error('Save story error:', err); alert('Failed to save story. Please try again.'); } }
// === UTILITY === function logout() { sessionPassword = null; localStorage.removeItem('builds-session'); allBuilds = []; allStories = []; document.getElementById('footer-lock').textContent = '🔒'; document.getElementById('logout-link').style.display = 'none'; location.reload(); }
// Event listeners document.getElementById('password').addEventListener('keypress', function(e) { if (e.key === 'Enter') checkPassword(); });
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeStoryModal(); });
setInterval(updateBuildTimes, 60000); </script></body></html>