2026-09-19 20:49:53 +00:00
import os
2026-10-30 14:24:30 +00:00
import random
2026-09-19 20:49:53 +00:00
import configparser
import requests
2026-10-30 02:56:22 +00:00
from flask import Flask , render_template_string , jsonify , send_from_directory
2026-09-19 20:49:53 +00:00
app = Flask ( __name__ )
OLLAMA_HOST = os . environ . get ( " OLLAMA_HOST " , " http://harbor.freshbrewed.science:11444 " )
OLLAMA_MODEL = os . environ . get ( " OLLAMA_MODEL " , " gemma4:e4b " )
2026-09-21 11:22:51 +00:00
PROMPT = os . environ . get ( " JOKE_PROMPT " , " tell me a dad joke about programming " )
2026-09-19 20:49:53 +00:00
PORT = int ( os . environ . get ( " PORT " , " 80 " ) )
2026-10-30 14:24:30 +00:00
PRESET_JOKES = [
" What do you call parenting with a software engineer? Pair programming (or O-pair programming). " ,
" My kids were rolling on the floor and screaming. My wife asked what was happening, and I told her it was a version conflict. " ,
" When I wrote this code, only God and I knew how it worked. Now, only God knows. " ,
" A programmer is sent to the store by his wife: “Get a gallon of milk, and if they have eggs, get a dozen.” The programmer returns home with 13 gallons of milk and says, “They had eggs”. " ,
" Where do coder dads store all their projects? In a dad-abase. " ,
" Why don ' t programmers like going outside into nature? Too many bugs. " ,
" Why did the programmer die taking a shower? The shampoo bottle instructions said: Lather. Rinse. Repeat. "
]
2026-09-19 20:49:53 +00:00
def get_app_version ( ) :
config = configparser . ConfigParser ( )
version_file = os . path . join ( os . path . dirname ( os . path . abspath ( __file__ ) ) , " version.ini " )
if os . path . exists ( version_file ) :
try :
config . read ( version_file )
return config . get ( " metadata " , " version " , fallback = " unknown " )
except Exception :
return " unknown "
return " unknown "
HTML_TEMPLATE = """ <!DOCTYPE html>
< html lang = " en " >
< head >
< meta charset = " UTF-8 " / >
< meta name = " viewport " content = " width=device-width, initial-scale=1.0 " / >
2026-10-30 02:56:22 +00:00
< meta name = " description " content = " Dry programming dad jokes set on a typographic studio proof sheet, pulled live from your local Ollama model. " / >
< title > Programming Dad Joke < / title >
< link rel = " preconnect " href = " https://fonts.googleapis.com " / >
< link rel = " preconnect " href = " https://fonts.gstatic.com " crossorigin / >
< link href = " https://fonts.googleapis.com/css2?family=Hedvig+Letters+Serif:opsz@12..24&family=Geist:wght@400;500&display=swap " rel = " stylesheet " / >
2026-09-19 20:49:53 +00:00
< style >
2026-10-30 02:56:22 +00:00
/ * Hallmark · shape : Workbench ( draw 9 cbbc9 ) · genre : editorial · theme : Studio ( deal 3 / 3 , seed programming - dad - joke - generator - 254 f · 3 aca28a0 · 2026 - 10 - 30 · r0 ) · redesign : full
* hero : Stage · nav : edge - aligned · footer : letter close · imagery : the product · signature : crop - marked proof sheet with orange metric construction guides
* axes : light / roman - serif / warm · posture : restrained · contrast : pass · fold : pass · v2 .0 .0 * /
/ * Hallmark · pre - emit critique : P5 H5 E5 S4 R5 V5 · weakest : S , generic joke card could fit any trivia app ; framed the joke as a crop - marked Studio proof sheet with live typographic metric guides * /
/ * Hallmark · reworded " Tell Me Another Joke! 🚀 " to " Pull another joke " · 30 a bans emoji in controls * /
/ * Hallmark · reworded " Freshly compiled humor from our AI model " to " Dry humor for engineers after hours, pulled live from your local model. " · 19 names generic AI filler * /
/ * Hallmark · dropped " Ollama • Gemma 4 " · 54 and 63 ban middle - dot eyebrow badges * /
@import url ( " ./tokens.css " ) ;
@import url ( " ./base.css " ) ;
. skip {
position : absolute ;
left : var ( - - space - md ) ;
top : calc ( - 1 * var ( - - space - 3 xl ) ) ;
z - index : var ( - - z - toast ) ;
padding : var ( - - space - xs ) var ( - - space - md ) ;
background : var ( - - color - ink ) ;
color : var ( - - color - paper ) ;
text - decoration : none ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. skip : focus - visible {
top : var ( - - space - md ) ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. proof {
background - image : radial - gradient ( circle at 1 px 1 px , var ( - - color - dot ) 1 px , transparent 1.4 px ) ;
background - size : 24 px 24 px ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. crop {
position : relative ;
- - l : 8 px ;
- - o : 12 px ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. crop : : before {
content : " " ;
position : absolute ;
inset : calc ( - 1 * var ( - - o ) ) ;
pointer - events : none ;
background :
linear - gradient ( var ( - - color - mark ) , var ( - - color - mark ) ) 0 var ( - - o ) / var ( - - l ) 1 px no - repeat ,
linear - gradient ( var ( - - color - mark ) , var ( - - color - mark ) ) var ( - - o ) 0 / 1 px var ( - - l ) no - repeat ,
linear - gradient ( var ( - - color - mark ) , var ( - - color - mark ) ) 100 % var ( - - o ) / var ( - - l ) 1 px no - repeat ,
linear - gradient ( var ( - - color - mark ) , var ( - - color - mark ) ) calc ( 100 % - var ( - - o ) ) 0 / 1 px var ( - - l ) no - repeat ,
linear - gradient ( var ( - - color - mark ) , var ( - - color - mark ) ) 0 calc ( 100 % - var ( - - o ) ) / var ( - - l ) 1 px no - repeat ,
linear - gradient ( var ( - - color - mark ) , var ( - - color - mark ) ) var ( - - o ) 100 % / 1 px var ( - - l ) no - repeat ,
linear - gradient ( var ( - - color - mark ) , var ( - - color - mark ) ) 100 % calc ( 100 % - var ( - - o ) ) / var ( - - l ) 1 px no - repeat ,
linear - gradient ( var ( - - color - mark ) , var ( - - color - mark ) ) calc ( 100 % - var ( - - o ) ) 100 % / 1 px var ( - - l ) no - repeat ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. site - header {
border - bottom : var ( - - rule - hair ) solid var ( - - color - rule ) ;
padding - block : var ( - - space - md ) ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. site - header__inner {
display : flex ;
align - items : center ;
justify - content : space - between ;
gap : var ( - - space - lg ) ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. wordmark {
font - family : var ( - - font - display ) ;
font - size : var ( - - text - lg ) ;
font - weight : var ( - - display - weight ) ;
letter - spacing : var ( - - tracking - display ) ;
color : var ( - - color - ink ) ;
text - decoration : none ;
white - space : nowrap ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. wordmark : hover {
text - decoration : underline ;
text - decoration - color : var ( - - color - accent ) ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. site - nav {
display : flex ;
2026-09-19 20:49:53 +00:00
align - items : center ;
2026-10-30 02:56:22 +00:00
gap : var ( - - space - lg ) ;
font - size : var ( - - text - sm ) ;
color : var ( - - color - ink - 2 ) ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. site - nav a {
text - decoration : none ;
white - space : nowrap ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. site - nav a : hover {
text - decoration : underline ;
text - decoration - color : var ( - - color - accent ) ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. hero {
padding - block : var ( - - space - 3 xl ) ;
border - bottom : var ( - - rule - hair ) solid var ( - - color - rule ) ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. hero__grid {
display : grid ;
grid - template - columns : minmax ( 0 , 5 fr ) minmax ( 0 , 7 fr ) ;
gap : var ( - - gap - columns ) ;
align - items : center ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. hero__lead {
display : flex ;
flex - direction : column ;
align - items : flex - start ;
gap : var ( - - space - lg ) ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. hero__lead . lead {
color : var ( - - color - ink - 2 ) ;
2026-09-19 20:49:53 +00:00
}
. actions {
display : flex ;
2026-10-30 02:56:22 +00:00
align - items : center ;
gap : var ( - - space - md ) ;
margin - top : var ( - - space - xs ) ;
2026-09-19 20:49:53 +00:00
}
. btn {
2026-10-30 02:56:22 +00:00
position : relative ;
2026-09-19 20:49:53 +00:00
appearance : none ;
display : inline - flex ;
align - items : center ;
2026-10-30 02:56:22 +00:00
justify - content : center ;
gap : var ( - - space - xs ) ;
min - height : 48 px ;
padding : 0 var ( - - space - lg ) ;
border : var ( - - rule - hair ) solid var ( - - color - ink ) ;
border - radius : var ( - - radius - card ) ;
background : var ( - - color - paper - hi ) ;
color : var ( - - color - ink ) ;
font - family : var ( - - font - body ) ;
font - size : var ( - - text - md ) ;
font - weight : 500 ;
line - height : 1 ;
white - space : nowrap ;
text - decoration : none ;
cursor : pointer ;
transition : background - color var ( - - dur - short ) var ( - - ease - out ) , transform var ( - - dur - micro ) var ( - - ease - out ) ;
}
. btn : : after {
content : " " ;
position : absolute ;
inset : var ( - - space - 3 xs ) ;
border : var ( - - rule - hair ) solid var ( - - color - mark ) ;
pointer - events : none ;
}
. btn - - primary {
background : var ( - - color - accent ) ;
color : var ( - - color - accent - ink ) ;
2026-09-19 20:49:53 +00:00
}
. btn : hover : not ( : disabled ) {
2026-10-30 02:56:22 +00:00
transform : translateY ( - 1 px ) ;
2026-09-19 20:49:53 +00:00
}
. btn : active : not ( : disabled ) {
transform : translateY ( 1 px ) ;
}
2026-10-30 02:56:22 +00:00
. btn : focus - visible {
outline : 2 px solid var ( - - color - focus ) ;
outline - offset : 2 px ;
}
. btn : disabled ,
. btn [ aria - disabled = " true " ] {
opacity : 0.55 ;
2026-09-19 20:49:53 +00:00
cursor : not - allowed ;
2026-10-30 02:56:22 +00:00
transform : none ;
}
. btn [ aria - busy = " true " ] {
cursor : progress ;
2026-09-19 20:49:53 +00:00
}
. spinner {
display : none ;
2026-10-30 02:56:22 +00:00
width : 16 px ;
height : 16 px ;
border : 2 px solid var ( - - color - mark ) ;
border - top - color : var ( - - color - ink ) ;
2026-09-19 20:49:53 +00:00
border - radius : 50 % ;
2026-10-30 02:56:22 +00:00
animation : spin var ( - - dur - long ) linear infinite ;
flex - shrink : 0 ;
2026-09-19 20:49:53 +00:00
}
@keyframes spin {
to { transform : rotate ( 360 deg ) ; }
}
2026-10-30 02:56:22 +00:00
. sheet {
background : var ( - - color - paper - hi ) ;
border : var ( - - rule - hair ) solid var ( - - color - rule ) ;
border - radius : var ( - - radius - card ) ;
box - shadow : var ( - - shadow - sheet ) ;
padding : var ( - - space - xl ) ;
2026-09-19 20:49:53 +00:00
display : flex ;
2026-10-30 02:56:22 +00:00
flex - direction : column ;
gap : var ( - - space - lg ) ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. guide - bar {
display : flex ;
2026-09-19 20:49:53 +00:00
align - items : center ;
2026-10-30 02:56:22 +00:00
gap : var ( - - space - sm ) ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. guide - line {
flex : 1 ;
height : var ( - - rule - hair ) ;
background : var ( - - color - accent ) ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. guide - readout {
font - size : var ( - - text - xs ) ;
font - weight : 500 ;
color : var ( - - color - muted ) ;
white - space : nowrap ;
}
. joke - specimen {
margin : 0 ;
padding - block : var ( - - space - md ) ;
border - top : var ( - - rule - hair ) solid var ( - - color - accent ) ;
border - bottom : var ( - - rule - hair ) solid var ( - - color - accent ) ;
}
. joke - text {
font - family : var ( - - font - display ) ;
font - size : var ( - - text - xl ) ;
font - weight : var ( - - display - weight ) ;
line - height : 1.4 ;
letter - spacing : var ( - - tracking - display ) ;
color : var ( - - color - ink ) ;
white - space : pre - wrap ;
overflow - wrap : break - word ;
min - height : var ( - - space - 3 xl ) ;
display : flex ;
align - items : center ;
transition : opacity var ( - - dur - short ) var ( - - ease - out ) ;
}
. sheet [ data - loading = " true " ] . joke - text {
opacity : 0.45 ;
}
. proof - progress {
display : flex ;
flex - direction : column ;
gap : var ( - - space - xs ) ;
}
. proof - progress__meta {
display : flex ;
align - items : baseline ;
justify - content : space - between ;
gap : var ( - - space - sm ) ;
font - size : var ( - - text - xs ) ;
font - weight : 500 ;
color : var ( - - color - muted ) ;
}
. proof - progress__track {
position : relative ;
height : var ( - - space - 2 xs ) ;
background : var ( - - color - paper - 3 ) ;
overflow : hidden ;
}
. proof - progress__fill {
width : 100 % ;
height : 100 % ;
background : var ( - - color - accent ) ;
transform - origin : left center ;
transform : scaleX ( var ( - - progress , 0 ) ) ;
transition : transform var ( - - dur - short ) var ( - - ease - out ) ;
}
. section - - notes {
padding - block : var ( - - section - gap - s ) ;
}
. section__head {
display : grid ;
grid - template - columns : minmax ( 0 , 5 fr ) minmax ( 0 , 7 fr ) ;
gap : var ( - - gap - columns ) ;
align - items : end ;
margin - bottom : var ( - - space - 2 xl ) ;
}
. section__head p {
color : var ( - - color - ink - 2 ) ;
}
. set {
list - style : none ;
margin : 0 ;
padding : 0 ;
border - top : var ( - - rule - hair ) solid var ( - - color - ink ) ;
counter - reset : rule - counter ;
}
. set__row {
counter - increment : rule - counter ;
display : grid ;
grid - template - columns : minmax ( 0 , 5 fr ) minmax ( 0 , 7 fr ) ;
gap : var ( - - gap - columns ) ;
align - items : baseline ;
padding - block : var ( - - space - lg ) ;
border - bottom : var ( - - rule - hair ) solid var ( - - color - rule ) ;
}
. set__row h3 : : before {
content : " 0 " counter ( rule - counter ) " " ;
font - family : var ( - - font - body ) ;
font - size : var ( - - text - sm ) ;
font - weight : 500 ;
color : var ( - - color - muted ) ;
margin - right : var ( - - space - sm ) ;
font - variant - numeric : tabular - nums ;
}
. set__row p {
color : var ( - - color - ink - 2 ) ;
}
. site - footer {
background : var ( - - color - paper - 2 ) ;
color : var ( - - color - ink ) ;
border - top : var ( - - rule - hair ) solid var ( - - color - ink ) ;
padding - block : var ( - - space - 2 xl ) ;
}
. footer__head {
display : flex ;
align - items : baseline ;
justify - content : space - between ;
gap : var ( - - space - lg ) ;
margin - bottom : var ( - - space - xl ) ;
flex - wrap : wrap ;
}
. footer__head p {
color : var ( - - color - ink - 2 ) ;
font - size : var ( - - text - sm ) ;
}
. colophon - grid {
margin : 0 ;
display : grid ;
grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) ;
gap : var ( - - gap - columns ) ;
border - top : var ( - - rule - hair ) solid var ( - - color - rule ) ;
padding - top : var ( - - space - lg ) ;
}
. colophon - item {
display : flex ;
flex - direction : column ;
gap : var ( - - space - 2 xs ) ;
min - width : 0 ;
}
. colophon - item dt {
font - size : var ( - - text - sm ) ;
font - weight : 500 ;
color : var ( - - color - ink - 2 ) ;
}
. colophon - item dd {
margin : 0 ;
font - size : var ( - - text - md ) ;
color : var ( - - color - ink ) ;
overflow - wrap : break - word ;
}
@media ( max - width : 60 rem ) {
. hero__grid ,
. section__head ,
. set__row {
grid - template - columns : minmax ( 0 , 1 fr ) ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
. colophon - grid {
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
2026-09-19 20:49:53 +00:00
}
2026-10-30 02:56:22 +00:00
}
@media ( max - width : 40 rem ) {
. colophon - grid {
grid - template - columns : minmax ( 0 , 1 fr ) ;
}
. sheet {
padding : var ( - - space - lg ) ;
}
}
@media ( prefers - reduced - motion : reduce ) {
. btn {
transition : background - color var ( - - dur - short ) linear ;
}
. joke - text ,
. proof - progress__fill {
transition : none ;
2026-09-19 20:49:53 +00:00
}
}
< / style >
< / head >
< body >
2026-10-30 02:56:22 +00:00
< a class = " skip " href = " #main " > Skip to content < / a >
< header class = " site-header proof " >
< div class = " container site-header__inner " >
< a href = " #main " class = " wordmark " aria - label = " Programming Dad Joke home " > Proof Sheet < / a >
< nav class = " site-nav " aria - label = " Page sections " >
< a href = " #specimen " > Specimen < / a >
< a href = " #notes " > Bench notes < / a >
< a href = " #colophon " > Colophon < / a >
< span class = " num " > v { { version } } < / span >
< / nav >
< / div >
< / header >
< main id = " main " >
< section class = " hero proof " >
< div class = " container hero__grid " >
< div class = " hero__lead " >
< h1 > Programming Dad Joke < / h1 >
< p class = " lead " > Dry humor for engineers after hours , pulled live from your local model . < / p >
< div class = " actions " >
< button id = " refreshBtn " class = " btn btn--primary crop " type = " button " onclick = " fetchNewJoke() " >
< span class = " spinner keep-motion " id = " spinner " aria - hidden = " true " > < / span >
< span id = " btnText " > Pull another joke < / span >
< / button >
< / div >
< / div >
< div id = " specimen " class = " hero__stage " >
< article id = " jokeSheet " class = " sheet crop " aria - live = " polite " aria - atomic = " true " >
< div class = " guide-bar " aria - hidden = " true " >
< span class = " guide-line " > < / span >
< span class = " guide-readout num " > Hedvig Letters Serif , 25 pt / 1.4 < / span >
< / div >
< blockquote class = " joke-specimen " >
< p id = " joke " class = " joke-text " > { { joke } } < / p >
< / blockquote >
< div id = " proofProgress " class = " proof-progress " hidden >
< div class = " proof-progress__meta " >
< span id = " progressStatus " > Setting type on the press ( 30 to 50 s typical ) … < / span >
< span id = " progressReadout " class = " num " > 0 % ( 0 s ) < / span >
< / div >
< div
id = " progressBar "
class = " proof-progress__track "
role = " progressbar "
aria - label = " Joke generation progress "
aria - valuemin = " 0 "
aria - valuemax = " 100 "
aria - valuenow = " 0 "
>
< div id = " progressFill " class = " proof-progress__fill " > < / div >
< / div >
< / div >
< div class = " guide-bar " aria - hidden = " true " >
< span class = " guide-readout num " > Proofed on { { model } } < / span >
< span class = " guide-line " > < / span >
< span class = " guide-readout num " > Release v { { version } } < / span >
< / div >
< / article >
< / div >
2026-09-19 20:49:53 +00:00
< / div >
2026-10-30 02:56:22 +00:00
< / section >
< section class = " section--notes " id = " notes " >
< div class = " container " >
< div class = " section__head " >
< h2 > Three rules on the press < / h2 >
< p class = " lead " > Every groaner on the sheet clears three checks before it leaves the studio . < / p >
< / div >
< ol class = " set " >
< li class = " set__row " >
< h3 > Pun on the literal < / h3 >
< p > Take a noun every developer says ten times before lunch , such as cache , branch , or deadlock , and read it as plain kitchen English . < / p >
< / li >
< li class = " set__row " >
< h3 > Keep the setup short < / h3 >
< p > One question across the counter . If the premise needs a whiteboard diagram , the punchline already missed its train . < / p >
< / li >
< li class = " set__row " >
< h3 > Aim for the eye roll < / h3 >
< p > A proper dad joke never chases an applause break . It settles for a quiet sigh from across the table . < / p >
< / li >
< / ol >
2026-09-19 20:49:53 +00:00
< / div >
2026-10-30 02:56:22 +00:00
< / section >
< / main >
< footer class = " site-footer " id = " colophon " >
< div class = " container " >
< div class = " footer__head " >
< h2 > Press colophon < / h2 >
< p > Compiled on demand from your local inference runtime . < / p >
2026-09-19 20:49:53 +00:00
< / div >
2026-10-30 02:56:22 +00:00
< dl class = " colophon-grid " >
< div class = " colophon-item " >
< dt > Host < / dt >
< dd class = " num " > { { host } } < / dd >
< / div >
< div class = " colophon-item " >
< dt > Model < / dt >
< dd class = " num " > { { model } } < / dd >
< / div >
< div class = " colophon-item " >
< dt > Prompt < / dt >
< dd > “ { { prompt } } ” < / dd >
< / div >
< div class = " colophon-item " >
< dt > App Version < / dt >
< dd class = " num " > v { { version } } < / dd >
< / div >
< / dl >
< / div >
< / footer >
2026-09-19 20:49:53 +00:00
< script >
2026-10-30 02:56:22 +00:00
let progressTimer = null ;
function computeExpectedProgress ( elapsedSeconds ) {
if ( elapsedSeconds < = 0 ) return 0.02 ;
if ( elapsedSeconds < = 30 ) {
return 0.02 + ( elapsedSeconds / 30 ) * 0.66 ;
}
if ( elapsedSeconds < = 50 ) {
return 0.68 + ( ( elapsedSeconds - 30 ) / 20 ) * 0.24 ;
}
return 0.92 + 0.06 * ( 1 - Math . exp ( - ( elapsedSeconds - 50 ) / 20 ) ) ;
}
function setProgressState ( ratio , elapsedSeconds , statusText , isComplete ) {
const progressBar = document . getElementById ( " progressBar " ) ;
const progressFill = document . getElementById ( " progressFill " ) ;
const progressReadout = document . getElementById ( " progressReadout " ) ;
const progressStatus = document . getElementById ( " progressStatus " ) ;
const clamped = Math . max ( 0 , Math . min ( 1 , ratio ) ) ;
const pct = Math . round ( clamped * 100 ) ;
const secs = isComplete
? Math . max ( 0.1 , elapsedSeconds ) . toFixed ( 1 )
: String ( Math . max ( 0 , Math . floor ( elapsedSeconds ) ) ) ;
progressFill . style . setProperty ( " --progress " , clamped . toFixed ( 4 ) ) ;
progressBar . setAttribute ( " aria-valuenow " , String ( pct ) ) ;
progressReadout . textContent = pct + " % ( " + secs + " s) " ;
if ( statusText ) {
progressStatus . textContent = statusText ;
}
}
2026-09-19 20:49:53 +00:00
async function fetchNewJoke ( ) {
const btn = document . getElementById ( " refreshBtn " ) ;
const btnText = document . getElementById ( " btnText " ) ;
const spinner = document . getElementById ( " spinner " ) ;
const jokeEl = document . getElementById ( " joke " ) ;
2026-10-30 02:56:22 +00:00
const sheet = document . getElementById ( " jokeSheet " ) ;
const progressWrap = document . getElementById ( " proofProgress " ) ;
const progressFill = document . getElementById ( " progressFill " ) ;
if ( progressTimer ) {
clearInterval ( progressTimer ) ;
progressTimer = null ;
}
2026-09-19 20:49:53 +00:00
btn . disabled = true ;
2026-10-30 02:56:22 +00:00
btn . setAttribute ( " aria-busy " , " true " ) ;
2026-09-19 20:49:53 +00:00
spinner . style . display = " inline-block " ;
2026-10-30 02:56:22 +00:00
btnText . textContent = " Pulling fresh proof \u2026 " ;
sheet . setAttribute ( " data-loading " , " true " ) ;
progressWrap . hidden = false ;
progressFill . style . transition = " none " ;
setProgressState ( 0 , 0 , " Setting type on the press (30 to 50s typical) \u2026 " , false ) ;
void progressFill . offsetWidth ;
progressFill . style . transition = " " ;
const startTime = performance . now ( ) ;
setProgressState ( 0.02 , 0 , " Setting type on the press (30 to 50s typical) \u2026 " , false ) ;
progressTimer = setInterval ( ( ) = > {
const elapsed = ( performance . now ( ) - startTime ) / 1000 ;
const ratio = computeExpectedProgress ( elapsed ) ;
const status =
elapsed < 30
? " Setting type on the press (30 to 50s typical) \u2026 "
: elapsed < = 50
? " Inking the proof sheet (30 to 50s window) \u2026 "
: " Finishing slow impression \u2026 " ;
setProgressState ( ratio , elapsed , status , false ) ;
} , 100 ) ;
let nextJoke = " " ;
2026-09-19 20:49:53 +00:00
try {
const response = await fetch ( " /api/joke " ) ;
if ( ! response . ok ) {
throw new Error ( " HTTP error " + response . status ) ;
}
const data = await response . json ( ) ;
2026-10-30 02:56:22 +00:00
nextJoke = data . joke | | " No joke returned. " ;
2026-09-19 20:49:53 +00:00
} catch ( err ) {
console . error ( " Failed to load joke: " , err ) ;
2026-10-30 02:56:22 +00:00
nextJoke = " Could not reach the joke service right now. Check the Ollama host and try again. " ;
2026-09-19 20:49:53 +00:00
} finally {
2026-10-30 02:56:22 +00:00
if ( progressTimer ) {
clearInterval ( progressTimer ) ;
progressTimer = null ;
}
const totalElapsed = ( performance . now ( ) - startTime ) / 1000 ;
setProgressState ( 1 , totalElapsed , " Proof complete (30 to 50s typical) " , true ) ;
jokeEl . textContent = nextJoke ;
sheet . removeAttribute ( " data-loading " ) ;
2026-09-19 20:49:53 +00:00
btn . disabled = false ;
2026-10-30 02:56:22 +00:00
btn . removeAttribute ( " aria-busy " ) ;
2026-09-19 20:49:53 +00:00
spinner . style . display = " none " ;
2026-10-30 02:56:22 +00:00
btnText . textContent = " Pull another joke " ;
2026-09-19 20:49:53 +00:00
}
}
< / script >
< / body >
< / html >
"""
def request_joke ( ) :
url = f " { OLLAMA_HOST . rstrip ( ' / ' ) } /api/generate "
payload = {
" model " : OLLAMA_MODEL ,
" prompt " : PROMPT ,
" stream " : False
}
try :
resp = requests . post ( url , json = payload , timeout = 60 )
resp . raise_for_status ( )
data = resp . json ( )
return data . get ( " response " , " No response received from Ollama. " )
except Exception as e :
return f " Error contacting Ollama ( { OLLAMA_HOST } ): { e } "
@app.route ( " / " )
def index ( ) :
2026-10-30 14:24:30 +00:00
joke = random . choice ( PRESET_JOKES )
2026-09-19 20:49:53 +00:00
version = get_app_version ( )
return render_template_string (
HTML_TEMPLATE ,
joke = joke ,
host = OLLAMA_HOST ,
model = OLLAMA_MODEL ,
prompt = PROMPT ,
version = version
)
2026-10-30 02:56:22 +00:00
@app.route ( " /tokens.css " )
def tokens_css ( ) :
return send_from_directory ( os . path . dirname ( os . path . abspath ( __file__ ) ) , " tokens.css " , mimetype = " text/css " )
@app.route ( " /base.css " )
def base_css ( ) :
return send_from_directory ( os . path . dirname ( os . path . abspath ( __file__ ) ) , " base.css " , mimetype = " text/css " )
2026-09-19 20:49:53 +00:00
@app.route ( " /api/joke " )
def api_joke ( ) :
joke = request_joke ( )
version = get_app_version ( )
return jsonify ( {
" joke " : joke ,
" version " : version
} )
if __name__ == " __main__ " :
version = get_app_version ( )
print ( f " Starting server v { version } on port { PORT } with model ' { OLLAMA_MODEL } ' at { OLLAMA_HOST } ... " )
app . run ( host = " 0.0.0.0 " , port = PORT )