diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,12 @@
+quarantimer (1.20200422) unstable; urgency=medium
+
+  * When time for surface is not known, as with masks, say that it's not,
+    instead of using a 1 year timeout.
+  * Added a stack.yaml file so it can be built using stack with a known
+    working set of packages.
+
+ -- Joey Hess <id@joeyh.name>  Wed, 22 Apr 2020 11:49:50 -0400
+
 quarantimer (1.20200326) unstable; urgency=medium
 
   * Incorportated new research from 
diff --git a/TODO b/TODO
--- a/TODO
+++ b/TODO
@@ -1,7 +1,27 @@
+* Timeline gets cluttered up with old items over time, and sometimes
+  there are several expired short timers before an active long one,
+  making it hard to notice it.
+
+  Might be best to split into two lists of active and expired.
+  However, that can't be done in the haskell as it stands now; would need
+  to be done in the js.
+
+* If two devices are displaying the same page, an add by one is not seen by
+  the other one until the page is reloaded.
+
+* Switch to an AJAX display where the js queries the server for a list of
+  items and then builds up the display? That would allow solving both
+  the cluttered timeline and multi-device update.
+
+  Alternatively, the js could make a long poll to the server, which replies
+  when it wants to update the display, and the js then just reloads the
+  page. This keeps the js smaller, and it also might replace the hourly
+  polling for a changed session id.
+
 * Check if the MediaDevices API is not suported and fall back to file
   upload.
 * Expire old images after some reasonable amount of time. This can be done
-  with just a find command.
+  with just a find command. Or the js could hide them.
 * Add systemd service file and Makefile to install it better.
 * Improve CSS. (Patches welcome on this one.)
 * The javascript could let things be added in offline mode, and sync up
diff --git a/quarantimer.cabal b/quarantimer.cabal
--- a/quarantimer.cabal
+++ b/quarantimer.cabal
@@ -1,5 +1,5 @@
 Name: quarantimer
-Version: 1.20200326
+Version: 1.20200422
 Cabal-Version: >= 1.8
 Maintainer: Joey Hess <id@joeyh.name>
 Author: Joey Hess
@@ -20,6 +20,7 @@
   CHANGELOG
   static/js.js
   static/style.css
+  stack.yaml
 
 Executable quarantimer
   Main-Is: quarantimer.hs
@@ -29,14 +30,14 @@
     , servant (>= 0.15 && < 0.18)
     , servant-server (>= 0.15 && < 0.18)
     , servant-client (>= 0.15 && < 0.18)
-    , servant-lucid (== 0.9.0.1)
+    , servant-lucid (== 0.9.*)
     , servant-multipart (== 0.11.*)
     , lucid (== 2.9.12)
     , aeson (>= 1.4.2 && < 1.5)
     , wai (== 3.2.*)
     , wai-extra
-    , warp (== 3.2.*)
-    , warp-tls (== 3.2.*)
+    , warp
+    , warp-tls
     , uuid (== 1.3.*)
     , bytestring
     , text
diff --git a/quarantimer.hs b/quarantimer.hs
--- a/quarantimer.hs
+++ b/quarantimer.hs
@@ -68,7 +68,7 @@
 	{ thingSurface :: Surface
 	, thingPhoto :: UrlString
 	, thingQuarantineStarted :: UTCTime
-	, thingSecondsUntilSafe :: Integer
+	, thingSecondsUntilSafe :: Maybe Integer
 	}
 	deriving (ToJSON, Generic)
 
@@ -76,7 +76,7 @@
 	{ surfaceID :: SurfaceID
 	, surfaceDesc :: T.Text
 	, surfaceIcon :: UrlString
-	, surfaceSecondsUntilSafe :: Integer
+	, surfaceSecondsUntilSafe :: Maybe Integer
 	, surfaceDeprecated :: Bool
 	}
 	deriving (ToJSON, Generic)
@@ -186,12 +186,17 @@
 					[ class_ "photo"
 					, src_ (T.pack (thingPhoto thing))
 					]
-				let timer = T.pack $ show $
-					thingSecondsUntilSafe thing
-				span_ 
-					[ class_ "timer"
-					, title_ timer
-					] (toHtml timer)
+				case thingSecondsUntilSafe thing of
+					Just s -> do
+						let timer = T.pack $ show s
+						span_ 
+							[ class_ "timer"
+							, title_ timer
+							] (toHtml timer)
+					Nothing -> do
+						span_ 
+							[ class_ "unknowntimer"
+							] "It is not known how long covid-19 remains on this surface."
 				br_ []
 				br_ []
 
@@ -315,19 +320,19 @@
 	-- https://www.nejm.org/doi/10.1056/NEJMc2004973
 	[ Surface (SurfaceID 1) "cardboard" 
 		(staticFileUrl "cardboard.jpg")
-		(hours 24) False
+		(Just (hours 24)) False
 	-- https://doi.org/10.1101/2020.03.15.20036673
 	-- tissue and printing paper
 	, Surface (SurfaceID 4) "paper"
 		(staticFileUrl "paper.jpg")
-		(hours 3) False
+		(Just (hours 3)) False
 	-- https://doi.org/10.1101/2020.03.15.20036673
 	, Surface (SurfaceID 5) "cloth"
 		(staticFileUrl "cloth.jpg")
-		(days 2) False
+		(Just (days 2)) False
 	, Surface (SurfaceID 2) "plastic"
 		(staticFileUrl "plastic.jpg")
-		(hours 72) False
+		(Just (hours 72)) False
 	-- https://doi.org/10.1101/2020.03.15.20036673
 	-- 7 days for stainless steel
 	--
@@ -339,17 +344,16 @@
 	-- not..
 	, Surface (SurfaceID 3) "metal"
 		(staticFileUrl "metal.jpg")
-		(days 7) False
+		(Just (days 7)) False
 	-- https://doi.org/10.1101/2020.03.15.20036673
 	-- found virus after 7 days in outer layer of surgical mask.
-	-- Unknown how long it lasts, and they should be discarded or
-	-- sterilized, so use 1 year as the timeout.
+	-- Unknown how long it lasts.
 	, Surface (SurfaceID 6) "surgical mask"
 		(staticFileUrl "mask.jpg")
-		(days 365) False
+		Nothing False
 	, Surface (SurfaceID 0) "other"
 		(staticFileUrl "other.jpg")
-		(hours 72) 
+		(Just (hours 72))
 		True -- do not offer as a selection any more
 	]
   where
@@ -425,7 +429,9 @@
 			mtime <- getModificationTime (userdir </> f)
 			now <- getCurrentTime
 			let secsold = max 0 (floor (diffUTCTime now mtime))
-			let ts = surfaceSecondsUntilSafe s - secsold
+			let ts = case surfaceSecondsUntilSafe s of
+				Nothing -> Nothing
+				Just ss -> Just (ss - secsold)
 			return $ Just $ Thing
 				{ thingSurface = s
 				, thingPhoto = staticUserFileUrl user f
diff --git a/stack.yaml b/stack.yaml
new file mode 100644
--- /dev/null
+++ b/stack.yaml
@@ -0,0 +1,5 @@
+packages:
+- '.'
+extra-deps:
+- sandi-0.5@sha256:b278d072ca717706ea38f9bd646e023f7f2576a778fb43565b434f93638849aa
+resolver: lts-14.27
diff --git a/static/js.js b/static/js.js
--- a/static/js.js
+++ b/static/js.js
@@ -17,9 +17,11 @@
 		things[i].img = img;
 		var timer = elt.getElementsByClassName("timer")[0];
 		things[i].timer = timer;
-		var n = parseInt(timer.attributes.title.value);
-		var d = loaddate + (n * 1000);
-		things[i].enddate = d;
+		if (timer != null) {
+			var n = parseInt(timer.attributes.title.value);
+			var d = loaddate + (n * 1000);
+			things[i].enddate = d;
+		}
 	}
 
 	animate();
@@ -97,18 +99,22 @@
 	for (var i = 0; i < things.length; i++) {
 		var img = things[i].img;
 		var timer = things[i].timer;
-		var diff = (things[i].enddate - now) / 1e3;
-		var h=(diff/60/60)>>0;
-		var m=(diff/60 - h*60)>>0;
-		if (diff > 0) {
-			img.className = "infected";
-			timer.innerHTML = "quarantine ends in " +
-				displayUnit(h,"hour") + " " +
-				displayUnit(m,"minute")
+		if (things[i].enddate != null) {
+			var diff = (things[i].enddate - now) / 1e3;
+			var h=(diff/60/60)>>0;
+			var m=(diff/60 - h*60)>>0;
+			if (diff > 0) {
+				img.className = "infected";
+				timer.innerHTML = "quarantine ends in " +
+					displayUnit(h,"hour") + " " +
+					displayUnit(m,"minute")
+			} else {
+				img.className = "safe";
+				timer.innerHTML = "quarantine completed " +
+					displayUnit((h*-1),"hour") + " ago"
+			}
 		} else {
-			img.className = "safe";
-			timer.innerHTML = "quarantine completed " +
-				displayUnit((h*-1),"hour") + " ago"
+			img.className = "infected";
 		}
 	}
 
