diff --git a/README b/README
--- a/README
+++ b/README
@@ -16,7 +16,7 @@
 the config file therein for configuration. An example
 configuration is included.
 
-darcsweb uses modules from these non-standard packages:
+DarcsWatch uses modules from these non-standard packages:
  * xhtml
  * network
  * HTTP
@@ -24,6 +24,10 @@
  * parsec
  * nano-md5
  * zlib
+ * concurrentoutput
+ * time
+ * mime-string
+ * regex-compat
 
 There are a lot of open issues, and help is very
 appreciated. A non exclusive list of TODO items is:
diff --git a/darcswatch.cabal b/darcswatch.cabal
--- a/darcswatch.cabal
+++ b/darcswatch.cabal
@@ -1,5 +1,5 @@
 Name:            darcswatch
-version:         0.3
+version:         0.4
 Category:        Distribution
 name:            darcswatch
 author:          Joachim Breitner
@@ -20,6 +20,10 @@
                  * Download link for each patch, to apply without searching for the email.
                  .
                  * Knows about inverse and amend-recorded patches and uses them to consider
+                 .
+                 * Integrates into http://bugs.darcs.net/
+                 .
+                 * Optionally sends out mails to mailing lists when a patch is applied to a repository
                  patches obsolete.
 Homepage:        http://darcswatch.nomeata.de/
 Cabal-Version:   >= 1.6
@@ -30,6 +34,10 @@
                  example/config
 Build-Type:      Simple
 
+Flag Static
+        Description: Build static binaries
+        Default: False
+
 Executable darcswatch-generate
         Main-Is:        Generate-Main.hs
         hs-source-dirs: src
@@ -70,26 +78,46 @@
                         , process
                         , mime-string
                         , regex-compat
+                        , cgi
+                        , mtl
+        if flag(Static)
+                ghc-options: -static -optl-static -optl-pthread                           
 
 Executable darcswatch-import-mail
         Main-Is:        ImportMail-Main.hs
         hs-source-dirs: src
+        if flag(Static)
+                ghc-options: -static -optl-static -optl-pthread                           
 
 Executable darcswatch-import-bundle
         Main-Is:        ImportBundle-Main.hs
         hs-source-dirs: src
+        if flag(Static)
+                ghc-options: -static -optl-static -optl-pthread                           
 
 Executable darcswatch-convert-data
         Main-Is:        ConvertData-Main.hs
         hs-source-dirs: src
+        if flag(Static)
+                ghc-options: -static -optl-static -optl-pthread                           
 
 Executable darcswatch-pull-repos
         Main-Is:        PullRepos-Main.hs
         hs-source-dirs: src
+        if flag(Static)
+                ghc-options: -static -optl-static -optl-pthread                           
 
 Executable darcswatch-update-data
         Main-Is:        UpdateRepoData-Main.hs
         hs-source-dirs: src
+        if flag(Static)
+                ghc-options: -static -optl-static -optl-pthread                           
+
+Executable darcswatch-cgi
+        Main-Is:        CGI-Main.hs
+        hs-source-dirs: src
+        if flag(Static)
+                ghc-options: -static -optl-static -optl-pthread                           
 
 source-repository head
   type:     darcs
diff --git a/documentation.html b/documentation.html
--- a/documentation.html
+++ b/documentation.html
@@ -46,7 +46,7 @@
 
 <h2>Other questions.</h2>
 
-<h3>What happens if the same mail reaches darcsweb multiple times, e.g. directly and via a mailing list?</h3>
+<h3>What happens if the same mail reaches DarcsWatch multiple times, e.g. directly and via a mailing list?</h3>
 <p>Not much. Patch bundles are stored by their MD5 sum, so if the same hash bundle arrives again, it will just override the old one.</p>
 
 <h3>I submitted a patch but it should not be used. Can I mark it as obsolete or rejected?</h3>
@@ -54,7 +54,7 @@
 
 <p> <strong>Via tagged mails</strong> DarcsWatch remembers the message-ids of the mails that submitted a particular patch bundle, so if it sees a direct reply to such a mail, it looks in the subject for the text <tt>[OBSOLETE]</tt> or <tt>[REJECTED]</tt> and marks a patch as such. If the bundle (or parts of it) are later re-submitted, these flags are removed again.</p>
 
-<p>If you don’t have the original message around to reply to it, you can also attach the patch bundle to the mail with the tag in the subject – just download it from the DarcsWatch site. Also, when patch bundles are added to DarcsWatch manually, there might not be a message-id attached and this will be the only way. The probably fastest way of doing that is using the command line <tt>GET http://darcswatch.nomeata.de/....dpatch | mail -s '[REJECTED]' darcswatch@nomeata.de</tt>.
+<p>If you don’t have the original message around to reply to it, you can also attach the patch bundle to the mail with the tag in the subject – just download it from the DarcsWatch site. Also, when patch bundles are added to DarcsWatch manually, there might not be a message-id attached and this will be the only way. <!-- The probably fastest way of doing that is using the command line <tt>GET http://darcswatch.nomeata.de/....dpatch | mail -s '[REJECTED]' darcswatch@nomeata.de</tt>. -->
 </p>
 
 <p> <strong>Via inverse patches.</strong> Make sure DarcsWatch has seen the inverse of the patch. You can do so by first running <tt>darcs rollback</tt> and select your patch, and then send it to DarcsWatch using <tt>darcs send --to=darcswatch@nomeata.de</tt>. If the project is using a mailing list that is being tracked by DarcsWatch, you can send this mail to the list, thus notifying your fellow developers that you consider your patch obsolete. Of course you can do this also to patches other than your own.</p>
diff --git a/example/config b/example/config
--- a/example/config
+++ b/example/config
@@ -12,5 +12,8 @@
 	, cMails = "/home/jojo/projekte/programming/haskell/darcswatch/example/mails"
 	, cDarcsWatchURL = "http://darcswatch.nomeata.de/"
 	, cDarcsWatchAddress = "darcswatch@nomeata.de"
-	, cSendRoundupMails = False
+	, cRepoSubscriptions =
+		[ ("http://code.haskell.org/XMonadContrib", "xmonad@haskell.org")
+		]
+	, cSendMails = False
 	}
diff --git a/src/CGI-Main.hs b/src/CGI-Main.hs
new file mode 100644
--- /dev/null
+++ b/src/CGI-Main.hs
@@ -0,0 +1,74 @@
+{-
+Copyright (C) 2009 Joachim Breitner
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2, or (at your option)
+any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; see the file COPYING.  If not, write to
+the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+Boston, MA 02110-1301, USA.
+-}
+
+import Data.List
+import Data.Maybe
+import System.Environment
+import System.FilePath
+import Network.CGI
+import Control.Applicative
+
+import Darcs.Watch.Storage
+import Darcs.Watch.Data
+import HTML
+
+main = do
+	confdir <- getEnv "HTTP_DARCSWATCH_DIR"
+
+        config <- read `fmap` readFile (confdir </> "config") :: IO DarcsWatchConfig
+
+	
+	runCGI $ handleErrors $ authenticated config $ \openID -> do
+
+	--setHeader "Content-type" "text/html; charset=UTF-8"
+
+	bundleChanges <- getBundleChanges <$> getInputs
+	if null bundleChanges
+	 then output $ cgiMessagePage True "No changed entered"
+	 else do errors <- catMaybes <$> mapM (applyBundleChange config openID) bundleChanges
+	         if null errors
+		  then output $ cgiMessagePage False $ "Sucessfully updated " ++
+			show (length bundleChanges) ++ " patch bundle state" ++
+			(if (length bundleChanges) == 1 then "" else "s")++ ". " ++
+			"The DarcsWatch web pages are generated " ++
+			"periodically, so it might take a while until your "++
+			"changes become visible."
+		  else output $ cgiMessagePage True $ "There were errors "++
+			"applying your updates: "++ show errors
+
+getBundleChanges = mapMaybe $ \(n,v) -> 
+	case ("state-" `stripPrefix` n, v) of
+		(Nothing,_) -> Nothing
+		(Just hb,"UNCHANGED") -> Nothing
+		(Just hb,"OBSOLETE") -> Just (hb, Obsoleted)
+		(Just hb,"REJECTED") -> Just (hb, Rejected)
+		_ -> Nothing 
+
+applyBundleChange config openID (bhash,newState) = do
+	history <- liftIO $ getBundleHistory (cData config) bhash
+	if newState <= maxState history then
+		return $ Just $
+			"Can not set patch bundle state to " ++ show newState
+			++", "++ "already in state " ++ show (maxState history) ++ "!"
+	  else do
+	  	liftIO $ changeBundleState (cData config) bhash (ViaWeb openID) newState
+		return Nothing
+
+authenticated config action = do
+	action "unauthenticated"
diff --git a/src/Darcs.hs b/src/Darcs.hs
--- a/src/Darcs.hs
+++ b/src/Darcs.hs
@@ -17,6 +17,7 @@
 Boston, MA 02110-1301, USA.
 -}
 
+{-# LANGUAGE BangPatterns #-}
 module Darcs
 	( PatchInfo(..)
 	, PatchBundle
@@ -29,6 +30,7 @@
 	, hash_bundle
 	, make_context
 	, scan_context
+	, make_patch
 	) where
 
 import OldDate
@@ -41,6 +43,8 @@
 import Printer
 import SHA1
 
+import Control.Arrow
+
 import qualified Data.ByteString.Char8 as B
 import Data.ByteString.Char8 (ByteString)
 import Data.List
@@ -71,11 +75,11 @@
 getInventory write cDir repo = do
 	format <- get write False cDir formatUrl
 	case format of
-		Nothing                                     -> getInventory1 write cDir repo
-		Just (f,_) | f == B.pack "hashed\ndarcs-2\n" -> getInventory2 write cDir repo
-		           | f == B.pack "darcs-1.0\n"      -> getInventory1 write cDir repo
-		           | f == B.pack "hashed\n"         -> getInventory2 write cDir repo
-		           | otherwise                      -> error $ "Unkown repository format: " ++ B.unpack f ++ " in repo " ++ repo
+		Nothing                           -> getInventory1 write cDir repo
+		Just (f,_) | f == litHashedDarcs2 -> getInventory2 write cDir repo
+		           | f == litDarcs10      -> getInventory1 write cDir repo
+		           | f == litHashed       -> getInventory2 write cDir repo
+		           | otherwise            -> error $ "Unkown repository format: " ++ B.unpack f ++ " in repo " ++ repo
   where	formatUrl = addSlash repo ++ "_darcs/format"
 
 -- | Gets called when old style format was detected
@@ -91,7 +95,7 @@
 	   let unzipped = maybeUnzipB body
 	   let patches = readPatchInfos unzipped
 	   case breakOn '\n' unzipped of
-	     (l1,r) | l1 == B.pack "Starting with tag:" -> do
+	     (l1,r) | l1 == litStartingWithTag -> do
 	     	let p = head patches
 		let filename = addSlash repo ++ "_darcs/inventories/" ++  patchBasename p ++ ".gz"
                 (prev_p,prev_u) <- getInventoryFile True filename
@@ -108,11 +112,11 @@
 			write $ "Repository " ++ repo ++ " not found.\n"
 			return ([],False)
 	skip_pristine s = case breakOn '\n' s of
-	                    (l1,r) | B.pack "pristine" `B.isPrefixOf` l1 -> B.tail r
+	                    (l1,r) | litPristine `B.isPrefixOf` l1 -> B.tail r
 			    _                                            -> s
 	parseStart body = do 
 	   case breakOn '\n' (skip_pristine body) of
-	     (l,r) | l == B.pack "Starting with inventory:" -> do
+	     (l,r) | l == litStartingWithInventory -> do
 	     	 case breakOn '\n' $ B.tail r of
 		   (h,r'') -> do prev <- getInventoryFile True (addSlash repo ++ "_darcs/inventories/" ++ B.unpack h)
 		                 return (prev,B.tail r'')
@@ -133,27 +137,28 @@
 			     Nothing -> []
 
 readPatchInfo :: ByteString -> Maybe (PatchInfo, ByteString)
-readPatchInfo s | B.null (dropWhite s) = Nothing
 readPatchInfo s =
-    if B.head (dropWhite s) /= '[' -- ]
+    if B.null s' || B.head s' /= '[' -- ]
     then Nothing
-    else case breakOn '\n' $ B.tail $ dropWhite s of
-         (name,s') | B.null s' -> error $ "Broken file (1) " ++ show (B.unpack s)
-         (name,s') | otherwise -> 
+    else case breakOn '\n' (B.tail s') of
+         (!name,s') | B.null s' -> error $ "Broken file (1) " ++ show (B.unpack s)
+                    | otherwise -> 
              case breakOn '*' $ B.tail s' of
-             (author,s2) | B.null s2 -> error "Broken file (2)"
+             (!author,s2) | B.null s2 -> error "Broken file (2)"
 	                 | otherwise -> 
                  case B.break (\c->c==']'||c=='\n') $ B.drop 2 s2 of
-                 (ct,s''') ->
-                     do (log, s4) <- lines_starting_with_ending_with ' ' ']' $ dn s'''
+                 (!ct,!s''') ->
+                     do (!log, !s4) <- lines_starting_with_ending_with ' ' ']' $ dn s'''
                         let not_star = B.index s2 1 /= '*'
-                        return $ (PatchInfo { piDate = ct
+			not_star `seq` return 
+				( PatchInfo { piDate = ct
                                             , piName = name
                                             , piAuthor = author
                                             , piLog = log
                                             , piInverted = not_star
                                             }, s4)
     where dn x = if B.null x || B.head x /= '\n' then x else B.tail x
+    	  s' = dropWhite s
 
 lines_starting_with_ending_with :: Char -> Char -> ByteString -> Maybe ([ByteString],ByteString)
 lines_starting_with_ending_with st en s = lswew s
@@ -165,17 +170,19 @@
     else if B.head x /= st
          then Nothing
          else case breakOn '\n' $ B.tail x of
-              (l,r) -> case lswew $ B.tail r of
-                       Just (ls,r') -> Just (l:ls,r')
+              (!l,r) -> case lswew $ B.tail r of
+                       Just (!ls,r') -> Just (l:ls,r')
                        Nothing ->
                            case breakLast en l of
-                           Just (l2,_) ->
-                               Just ([l2],  B.drop (B.length l2+2) x)
+                           Just (!l2,_) ->
+			   	let rest = B.drop (B.length l2+2) x in
+                           	rest `seq` Just ([l2],  B.drop (B.length l2+2) x)
                            Nothing -> Nothing
 
 
 dropWhite = B.dropWhile (`elem` " \n\t\r")
-breakOn c = B.break (c ==)
+breakOn :: Char -> ByteString -> (ByteString, ByteString)
+breakOn c = B.break (==c)
 
 breakLast c p = case B.elemIndexEnd c p of
     Nothing -> Nothing
@@ -202,6 +209,10 @@
 showPatch :: (PatchInfo,ByteString) -> Doc
 showPatch (pi,d) = showPatchInfo pi <> packedString d
 
+
+make_patch :: PatchInfo -> ByteString
+make_patch = renderPS . showPatchInfo
+
 make_bundle :: PatchBundle -> ByteString
 make_bundle bundle@(to_be_sent, common) = renderPS $
                            text ""
@@ -258,8 +269,7 @@
 
 
 silly_lex :: ByteString -> (String, ByteString)
-silly_lex ps = (B.unpack $ B.takeWhile (/='\n') $ dropWhite ps,
-                           B.dropWhile (/='\n') $ dropWhite ps)
+silly_lex = first B.unpack . B.span (/='\n') . dropWhite
 
 make_context :: [PatchInfo] -> ByteString
 make_context = renderPS . vcat . map showPatchInfo
@@ -277,20 +287,22 @@
 filter_gpg_dashes :: ByteString -> ByteString
 filter_gpg_dashes ps =
     B.unlines $ map drop_dashes $
-    takeWhile (/= B.pack "-----END PGP SIGNED MESSAGE-----") $
+    takeWhile (/= litEndPGPSignedMessages) $
     dropWhile not_context_or_newpatches $ B.lines ps
     where drop_dashes x = if B.length x < 2 then x
-                          else if B.take 2 x == B.pack "- "
+                          else if B.take 2 x == litDashSpace
                                then B.drop 2 x
                                else x
-          not_context_or_newpatches s = (s /= B.pack "Context:") &&
-                                        (s /= B.pack "New patches:")
+          not_context_or_newpatches s = (s /= litContext) &&
+                                        (s /= litNewPatches)
 
 readDiff :: ByteString -> Maybe (ByteString, ByteString)
-readDiff s | B.null (dropWhite s) = Nothing
-readDiff s = find (\(p,r) -> B.pack "\n\n" `B.isPrefixOf` r || B.pack "\n[" `B.isPrefixOf` r)
+readDiff s = 
+	if B.null s' then Nothing
+	else find (\(p,r) -> litNewlineNewline `B.isPrefixOf` r
+ 	                  || litNewlineBracket `B.isPrefixOf` r)
                   (zip (B.inits s') (B.tails s'))
-	where s' = dropWhite s
+  where	s' = dropWhite s
 
 patchFilename :: PatchInfo -> String
 patchFilename pi = patchBasename pi ++ ".gz"
@@ -299,8 +311,8 @@
 --   stores it in, without the ".gz" suffix.
 patchBasename :: PatchInfo -> String
 patchBasename pi = showIsoDateTime d++"-"++sha1_a++"-"++sha1 (B.unpack sha1_me)
-        where b2ps True = B.pack "t"
-              b2ps False = B.pack "f"
+        where b2ps True  = litT
+	      b2ps False = litF
               sha1_me = B.concat [piName pi,
                                   piAuthor pi,
                                   piDate pi,
@@ -319,3 +331,23 @@
 
 addSlash filename | last filename == '/' = filename
                   | otherwise            = filename ++ "/"
+
+-- Packed bytestring literators, to avoid re-packing them constantly (ghc is
+-- probably not smart enough to do it by itself 
+
+litHashedDarcs2 = B.pack "hashed\ndarcs-2\n"
+litDarcs10 = B.pack "darcs-1.0\n"
+litHashed = B.pack "hashed\n"
+litStartingWithTag = B.pack "Starting with tag:"
+litPristine = B.pack "pristine"
+litStartingWithInventory = B.pack "Starting with inventory:"
+litEndPGPSignedMessages = B.pack "-----END PGP SIGNED MESSAGE-----"
+litDashSpace = B.pack "- "
+litContext = B.pack "Context:"
+litNewPatches = B.pack "New patches:"
+litNewlineNewline = B.pack "\n\n" 
+litNewlineBracket = B.pack "\n[" 
+litT = B.pack "t"
+litF = B.pack "f"
+
+
diff --git a/src/Darcs/Watch/Data.hs b/src/Darcs/Watch/Data.hs
--- a/src/Darcs/Watch/Data.hs
+++ b/src/Darcs/Watch/Data.hs
@@ -20,6 +20,7 @@
 module Darcs.Watch.Data where
 
 import Data.Time
+import Data.Maybe
 
 type BundleHash = String
 
@@ -27,7 +28,7 @@
 type StorageConf = String
 
 -- | A history entry is a state change
-type BundleHistory = (UTCTime, Source, BundleState)
+type BundleHistory = (ZonedTime, Source, BundleState)
 
 data BundleState
 	= New
@@ -41,12 +42,20 @@
 	= ManualImport
 	| ViaEMail String String String (Maybe String) -- ^ From, To, Subject, Message-Id 
 	| ViaBugtracker String -- ^ URL
---	| ViaWeb String -- ^ OpenID Username
+	| ViaWeb String -- ^ OpenID Username
 	| ViaRepository RepositoryURL -- ^ Repository-URL
-	deriving (Read, Show)
+	deriving (Read, Show, Eq, Ord)
 
 type RepositoryURL = String
+type EMailAddress = String -- ^ not use for patch authors, but subscriptions etc.
 
+type Author = String -- ^ the email-address of an patch author
+
+data BundleList = RepositoryBundleList RepositoryURL
+                | AuthorBundleList Author
+		| UnmatchedBundleList
+	deriving (Read, Show, Eq, Ord)
+
 data RepositoryInfo = RepositoryInfo 
 	{ lastCheck :: Maybe UTCTime
 	, lastUpdate :: Maybe UTCTime
@@ -60,6 +69,12 @@
         cMails :: String,
 	cDarcsWatchURL :: String,
 	cDarcsWatchAddress :: String,
-	cSendRoundupMails :: Bool
+	cRepoSubscriptions :: [(RepositoryURL, EMailAddress)],
+	cSendMails :: Bool
         } deriving (Show, Read)
 
+
+stateOfRepo history repo = fromMaybe New (lookup repo (repoStates history))
+
+repoStates history = [ (repo, state)
+	             | (_,ViaRepository repo, state) <- history ]
diff --git a/src/Darcs/Watch/GenerateOutput.hs b/src/Darcs/Watch/GenerateOutput.hs
--- a/src/Darcs/Watch/GenerateOutput.hs
+++ b/src/Darcs/Watch/GenerateOutput.hs
@@ -36,6 +36,9 @@
 import Data.Char
 import Data.List
 import Data.Time
+import Data.Digest.OpenSSL.MD5 (md5sum)
+import Data.Maybe
+import Data.Monoid
 import System.FilePath
 
 import qualified Data.ByteString.Char8 as B
@@ -48,9 +51,6 @@
 -- Web ouput
 import HTML
 
-import Data.Digest.OpenSSL.MD5 (md5sum)
-import Data.Maybe
-
 generateOutput config patchNew = do
 	nowStamp <- getCurrentTime
 	let outputStampFile = cData config </> "output.stamp"
@@ -58,110 +58,86 @@
 	lastStamp <- if ex then read . B.unpack <$> B.readFile outputStampFile
 	                   else return $ UTCTime (ModifiedJulianDay 0) 0
 
-        putStrLn "Reading repositories..."
-	let loadInv rep = do
-                putStr $ "Reading " ++ rep ++ " ...\n"
-		ps <- readRepository (cData config) rep
-		repoInfo <- getRepositoryInfo (cData config) rep
-		let thisNew = maybe True (>= lastStamp) (lastUpdate repoInfo)
-		putStr (if thisNew then "Repostory is new.\n" else "Repository is cached.\n")
-		return (rep, ps, repoInfo, thisNew)
-            readInv (p2r,r2p,r2ri,new) (rep, ps, repoInfo, thisNew) = do
-                let p2r' = foldr (\p -> MM.append p rep) p2r ps
-                    r2p' = MM.extend rep ps r2p :: M.Map RepositoryURL (S.Set PatchInfo)
-		    r2ri' = M.insert rep repoInfo r2ri
-                return (p2r', r2p', r2ri', new || thisNew)
-        (p2r,r2p, r2ri, new) <- foldM readInv (MM.empty, MM.empty, M.empty, patchNew) =<<
-                          sequence (map loadInv (cRepositories config))
+	date <- getClockTime >>= toCalendarTime
 
-        putStrLn "Reading emails..."
-	bundleHashes <- listBundles (cData config)
+	putStrLn "Reading name mapping list..."
+	nameMapping <- readNameMapping (cData config)
 
-        let sortInBundle (u2p, u2rn, p2pe, new) bundleHash = do
-                putStrLn $ "Reading mail " ++ bundleHash ++ " ..."
+	putStrLn "Reading bundle list lists.."
+	bundleLists <- getBundleListList (cData config)
+	-- Split bundle lists by type
+	let (repos, authors) = mconcat $ map (\bl -> case bl of
+		RepositoryBundleList repo -> ([repo],[])
+		AuthorBundleList author -> ([],[author])
+		_ -> mempty) bundleLists
+	
+	let getBundleInfos bundleHash = do
 		bundle <- getBundle (cData config) bundleHash
-		let bundleFileName = getBundleFileName (cData config) bundleHash
 		history <- getBundleHistory (cData config) bundleHash
+		let bundleFileName = "bundles" </> bundleBaseName bundleHash
 		
-		let thisNew = any (\(d,_,_) -> d >= lastStamp) history
-
-                let (patches,context) = bundle
-                let u2p' = foldr (\(p,_) -> MM.append (normalizeAuthor (piAuthor p)) p) u2p patches
-                let u2rn' = foldr (\(p,_) ->
-                        M.insertWith (maxBy B.length) (normalizeAuthor (piAuthor p)) (piAuthor p)
-                        ) u2rn patches
-                let p2pe' = foldr (\(p,d) ->
-                        let pe = PatchExtras d context bundleFileName history
-                        -- The patch with the smaller context is the more useful
-                        in  M.insertWith (minBy (length.peContext)) p pe
-                        ) p2pe patches
-                return (u2p', u2rn', p2pe', new || thisNew)
-        (u2p, u2rn, p2pe, new') <- foldM sortInBundle (MM.empty, M.empty, M.empty, new) bundleHashes
-        let patches = M.keys p2pe -- Submitted patches
-        let repos   = M.keys r2p -- Repos with patches
-        let users   = M.keys u2p -- Known users
-        
-	if not new' then putStrLn "Nothing new, exiting" else do
-
-
-	-- Clonsider patches as belonging to a repository when either
-	--  * it is already applied
-	--  * all its context is in the repository
-	--  * at least 10 patches of the context are in the repository
-        let addables = do -- List monad
-                patch <- patches
-                repo  <- repos
-                present <- maybeToList $ M.lookup repo r2p
-		if patch `S.member` present
-                  then return (patch,repo)
-		  else do
-			pe <- maybeToList $ M.lookup patch p2pe
-			case partition (`S.member` present) (peContext pe) of
-				(_,[]) -> return (patch,repo)	
-				(m,_) | length m >= 10 -> return (patch,repo)	
-				_ -> []
-
-        -- Patch to possible repos
-        -- Repo to possible patch
-        let p2pr = foldr (\(p,r) -> MM.append p r) MM.empty addables
-        let r2mp = foldr (\(p,r) -> MM.append r p) MM.empty addables
-
-        -- Unmatched patches
-        let unmatched = S.fromList $
-			-- filter (\p -> M.findWithDefault Unmatched p p2s == Unmatched) $
-                        filter (\p -> not (M.member p p2pr)) patches
-
-        now <- getClockTime >>= toCalendarTime
-        let resultData = ResultData p2r r2p u2p p2pe p2pr r2mp r2ri unmatched now u2rn
-	
-	{-
-	putStrLn "Evalutating data"
-	putStrLn (show (length (show (resultData))))
-	-}
-
-        putStrLn "Writing output..."
-        writeFile (cOutput config ++ "/index.html") (mainPage resultData)
+		return (BundleInfo bundleHash bundle bundleFileName history)
 
-        forM_ users $ \u ->
-                writeFile (cOutput config ++ "/" ++ userFile u) (userPage resultData u)
+        putStrLn "Writing output (repo pages)..."
+        forM_ repos $ \r -> do
+		(bundleHashes,_) <- readBundleList (cData config) (RepositoryBundleList r)
+		bundleInfos <- mapM getBundleInfos bundleHashes
+		repoInfo <- getRepositoryInfo (cData config) r
+                writeFile (cOutput config ++ "/" ++ repoFile r) $
+			repoPage date nameMapping r repoInfo bundleInfos
 
-        forM_ repos $ \r ->
-                writeFile (cOutput config ++ "/" ++ repoFile r) (repoPage resultData r)
+        putStrLn "Writing output (user pages)..."
+        forM_ authors $ \u -> do
+		(bundleHashes,_) <- readBundleList (cData config) (AuthorBundleList u)
+		bundleInfos <- mapM getBundleInfos bundleHashes
+                writeFile (cOutput config ++ "/" ++ userFile u) $
+			userPage date nameMapping u bundleInfos
+        
+	putStrLn "Writing output (main page)..."
+	bundleHashes <- listBundles (cData config)
 
-        forM_ patches $ \p ->
-                B.writeFile (cOutput config ++ "/" ++ patchDiffFile p) (peDiff (p2pe M.! p))
+	patches <- mconcat <$> forM bundleHashes (\bundleHash -> do
+		(ps,_) <- getBundle (cData config) bundleHash
+		return (M.fromList ps)
+		)
+	let patchCount = M.size patches
+	let bundleCount = length bundleHashes
+	let strict5 f a b c d e = ((((f $! a) $! b) $! c) $! d ) $! e
+	let strict6 f a b c d e g = (((((f $! a) $! b) $! c) $! d ) $! e) $! g
+	repoData <- forM repos $ \r -> do
+		(bundleHashes,_) <- readBundleList (cData config) (RepositoryBundleList r)
+		bundleInfos <- mapM getBundleInfos bundleHashes
+		inv <- readRepository (cData config) r
+		return $ strict6 (,,,,,) 
+			r
+			(length inv)
+			(length $ bundleInfoFilter Applied bundleInfos)
+			(length $ bundleInfoFilter Applicable bundleInfos)
+			(length $ bundleInfoFilter Obsoleted bundleInfos)
+			(length $ bundleInfoFilter Rejected bundleInfos)
+	userData <- forM authors $ \u -> do
+		(bundleHashes,_) <- readBundleList (cData config) (AuthorBundleList u)
+		bundleInfos <- mapM getBundleInfos bundleHashes
+		return $ strict5 (,,,,) 
+			u
+			(length bundleInfos)
+			(length (bundleInfoFilter Applicable bundleInfos))
+			(length (bundleInfoFilter Obsoleted bundleInfos))
+			(length (bundleInfoFilter Rejected bundleInfos))
+        writeFile (cOutput config ++ "/index.html") $
+		mainPage date nameMapping patchCount bundleCount repoData userData
 
-	writeFile (cOutput config ++ "/" ++ "unmatched.html") (unmatchedPage resultData)
+        putStrLn "Writing output (diffs)..."
+        forM_ (M.toList patches) $ \(p,d) -> do
+		let filename = cOutput config ++ "/" ++ patchDiffFile p 
+		ex <- doesFileExist filename
+		unless ex $ do
+			putStrLn $ "Writing new patch file " ++ filename
+			B.writeFile filename d
 
-        putStrLn "Linking patches"
-        let patchLink (p,pe) = do
-                let link = cOutput config ++ "/" ++ patchBasename p ++ ".dpatch"
-                ex <- fileExist link
-                unless ex $ do
-                        -- There might be a broken symlink here:
-                        catch (removeFile link) (const (return ()))
-                        createSymbolicLink (peBundleFile pe) link
-        mapM_ patchLink $ M.toList p2pe
+	ex <- doesDirectoryExist (cOutput config </> "bundles")
+	unless ex $ 
+		createSymbolicLink (bundleDir (cData config)) (cOutput config </> "bundles")
 
 	B.writeFile outputStampFile (B.pack (show nowStamp))
 
diff --git a/src/Darcs/Watch/ImportMail.hs b/src/Darcs/Watch/ImportMail.hs
--- a/src/Darcs/Watch/ImportMail.hs
+++ b/src/Darcs/Watch/ImportMail.hs
@@ -37,6 +37,7 @@
 
 import Darcs.Watch.Data
 import Darcs.Watch.Storage
+import Darcs.Watch.Roundup
 
 importMail :: DarcsWatchConfig -> IO Bool
 importMail config  = do
@@ -60,6 +61,9 @@
 			case roundupURL of
 			  Nothing -> return ()
 			  Just url -> do
+				history <- getBundleHistory (cData config) bhash
+				forM_ (repoStates history) $ \(repo,state) -> 	
+					tellRoundup config url repo bhash bundle state
 			  	changeBundleState (cData config) bhash
 			  		(ViaBugtracker url) New
 			changeBundleState (cData config) bhash emailSource state
@@ -79,7 +83,6 @@
 			return (or changes)
 
 findSubmittingMail = mapMaybe isSubmittingMail
-
 isSubmittingMail (_,ViaEMail _ _ _ (Just mid), New) = Just mid
 isSubmittingMail _ = Nothing
 
@@ -121,6 +124,7 @@
 findDarcsBundle (Message _ _ (Body (ContentType "text" "x-darcs-patch" _) _ msg)) = Just msg
 findDarcsBundle (Message _ _ (Data _ _ filename msg))
 	| ".dpatch" `isSuffixOf` filename = Just msg
+	| "darcs" `isPrefixOf` filename = Just msg
 findDarcsBundle (Message _ _ (Mixed (Multipart _ msgs _ ))) = msum (map findDarcsBundle msgs)
 findDarcsBundle (Message _ _ (Alternative (Multipart _ msgs _ ))) = msum (map findDarcsBundle msgs)
 findDarcsBundle (Message _ _ (Parallel (Multipart _ msgs _ ))) = msum (map findDarcsBundle msgs)
diff --git a/src/Darcs/Watch/Roundup.hs b/src/Darcs/Watch/Roundup.hs
--- a/src/Darcs/Watch/Roundup.hs
+++ b/src/Darcs/Watch/Roundup.hs
@@ -11,36 +11,32 @@
 import Darcs
 import HTML
 
-tellRoundup :: DarcsWatchConfig -> String -> RepositoryURL -> PatchBundle -> BundleState -> IO ()
-tellRoundup _      _   _    _      status | status `notElem` [Applied, Applicable] = return ()
-tellRoundup _      url _    _      _      | not $ "http://bugs.darcs.net/" `isPrefixOf` url = return ()
-tellRoundup config url repo bundle status = do
+tellRoundup :: DarcsWatchConfig -> String -> RepositoryURL -> BundleHash -> PatchBundle -> BundleState -> IO ()
+tellRoundup _      _   _    _          _      status | status `notElem` [Applied, Applicable] = return ()
+tellRoundup _      url _    _          _      _      | not $ "http://bugs.darcs.net/" `isPrefixOf` url = return ()
+tellRoundup config url repo bundleHash bundle status = do
 	message <- flatten [mk_header ["From: " ++ from]
 	                   ,mk_header ["To: " ++ to]
 			   ,mk_header ["Subject: " ++ subject]
 			   ] body Nothing []
-	if cSendRoundupMails config
+	if cSendMails config
 	  then do sendMail message
 	  else do hPutStrLn stderr "Would send this message:"
 	          hPutStrLn stderr message
   where from = cDarcsWatchAddress config
         to = "bugs@darcs.net"
 	subject = case status of
-	 	   Applicable -> "This patch is being tracked by DarcsWatch [" ++ roundupId ++ "]"
+	 	   Applicable -> "This patch is being tracked by DarcsWatch [" ++ roundupId ++ "] [darcswatchurl=" ++ bundleLink ++ "]"
 		   Applied -> "This patch has been applied [" ++ roundupId ++ "] [status=accepted]"
 	body = case status of
-		Applicable -> "This patch bundle (with " ++ show numPatches ++ " patch" ++
-			      (if numPatches == 1 then "" else "es") ++ "), which can be " ++
-			      " applied to the repository " ++ repo ++ " is now tracked " ++
-			      " on DarcsWatch at " ++
-                              cDarcsWatchURL config ++ repoFile repo
+		Applicable -> "" -- no messages about this, please
 		Applied ->    "This patch bundle (with " ++ show (length (fst bundle)) ++
 			      " patches) was just applied to the repository " ++ repo ++".\n" ++
 			      "This message was brought to you by " ++
 			      "DarcsWatch\n" ++
-                              cDarcsWatchURL config ++ repoFile repo
+			      bundleLink
+	bundleLink = bundleURL config repo bundleHash
         numPatches = length (fst bundle)
-
 	roundupId = drop (length "http://bugs.darcs.net/") url
 
 sendMail ::  String -> IO ()
diff --git a/src/Darcs/Watch/Storage.hs b/src/Darcs/Watch/Storage.hs
--- a/src/Darcs/Watch/Storage.hs
+++ b/src/Darcs/Watch/Storage.hs
@@ -24,6 +24,7 @@
 
 import Data.Time
 import qualified Data.ByteString.Char8 as B
+import qualified Data.Map as M
 import Data.ByteString.Char8 (ByteString)
 import System.FilePath
 import System.Directory
@@ -41,7 +42,7 @@
 	let dataFile = bundleDir path </> hash <.> "data"
 	ex <- doesFileExist dataFile
 	unless ex $ B.writeFile dataFile (B.pack "[]")
-	B.writeFile (bundleDir path </> hash <.> "bundle")
+	B.writeFile (bundleFileName path hash)
 	            (make_bundle bundle)
 	return hash
 
@@ -49,7 +50,7 @@
 -- | Retrieves a new patch bundle from the storage
 getBundle :: StorageConf -> BundleHash -> IO PatchBundle
 getBundle path hash = 
-	either error id . scan_bundle <$> B.readFile (bundleDir path </> hash <.> "bundle")
+	either error id . scan_bundle <$> B.readFile (bundleFileName path hash)
 
 -- | Retrieves the meta information for the handle
 getBundleHistory :: StorageConf -> BundleHash -> IO [BundleHistory]
@@ -60,7 +61,7 @@
 changeBundleState :: StorageConf -> BundleHash -> Source -> BundleState -> IO ()
 changeBundleState path hash source state = do
 	history <- getBundleHistory path hash
-	now <- getCurrentTime
+	now <- getZonedTime
 	B.writeFile (bundleDir path </> hash <.> "data") $ B.pack $ show $
 		(now, source, state) : history
 		
@@ -69,7 +70,7 @@
 listBundles path = do
 	items <- getDirectoryContents (bundleDir path)
 	return $ map dropExtension
-	       $ filter ( (".bundle" == ) .takeExtension )
+	       $ filter ( (".dpatch" == ) . takeExtension )
 	       $ items
 
 
@@ -104,17 +105,68 @@
 	if ex then read . B.unpack <$> B.readFile infoFile
 	      else return (RepositoryInfo Nothing Nothing)
 
+setBundleListList :: StorageConf -> [BundleList] -> IO ()
+setBundleListList path bll = do
+	writeFile (bundleListDir path </> "index")  (show bll)
+
+getBundleListList :: StorageConf -> IO [BundleList]
+getBundleListList path = do
+	read <$> B.unpack <$> B.readFile (bundleListDir path </> "index")
+
+writeBundleList :: StorageConf -> BundleList -> [BundleHash] -> IO ()
+writeBundleList path bl blist = do
+	let filename = bundleListFilename path bl
+	    sorted = sort blist
+	(oldList,_) <- readBundleList path bl
+	when (oldList /= blist) $ do
+		now <- getCurrentTime
+		writeFile filename (unlines (show now : blist))
+
+readBundleList :: StorageConf -> BundleList -> IO ([BundleHash], UTCTime)
+readBundleList path bl = do
+	let filename = bundleListFilename path bl
+	ex <- doesFileExist filename	
+	if not ex then return ([],UTCTime (ModifiedJulianDay 0) 0) else do
+		(stamp:bhashes) <- lines <$> B.unpack <$> B.readFile filename
+		return (bhashes, read stamp)
+
+writeNameMapping :: StorageConf -> M.Map Author String -> IO ()
+writeNameMapping path map = do
+	B.writeFile (nameMappingFilename path) $ B.pack $ show map
+
+readNameMapping :: StorageConf -> IO (M.Map Author String)
+readNameMapping path = do
+	let filename = nameMappingFilename path
+	ex <- doesFileExist filename	
+	if not ex then return M.empty else 
+		read <$> B.unpack <$> B.readFile filename
+
+bundleListFilename path (RepositoryBundleList repo) =
+	bundleListDir path </> "repo_" ++ safeName repo
+bundleListFilename path (AuthorBundleList author) =
+	bundleListDir path </> "user_" ++ safeName author
+bundleListFilename path UnmatchedBundleList =
+	bundleListDir path </> "unmatched"
+
+nameMappingFilename path = path </> "nameMapping"
+
 bundleDir :: StorageConf -> FilePath
 bundleDir path = path </> "bundles"
 
-getBundleFileName :: StorageConf -> BundleHash -> FilePath
-getBundleFileName path hash  = bundleDir path </> hash <.> "bundle"
+bundleBaseName :: BundleHash -> FilePath
+bundleBaseName hash = hash <.> "dpatch"
 
+bundleFileName :: StorageConf -> BundleHash -> FilePath
+bundleFileName path hash  = bundleDir path </> bundleBaseName hash
+
 repoDir :: StorageConf -> FilePath
 repoDir path = path </> "repos"
 
 cacheDir :: StorageConf -> FilePath
 cacheDir path = path </> "cache"
+
+bundleListDir :: StorageConf -> FilePath
+bundleListDir path = path </> "bundleLists"
 
 safeName n = map s n
   where s c = if isSafeFileChar c then c else '_'
diff --git a/src/Darcs/Watch/UpdateRepoData.hs b/src/Darcs/Watch/UpdateRepoData.hs
--- a/src/Darcs/Watch/UpdateRepoData.hs
+++ b/src/Darcs/Watch/UpdateRepoData.hs
@@ -41,13 +41,34 @@
 import Data.Digest.OpenSSL.MD5 (md5sum)
 import Data.Maybe
 import System.FilePath
+import Control.Monad.Writer
 
 -- Darcs stuff
 import Darcs
 import Darcs.Watch.Storage
 import Darcs.Watch.Data
 import Darcs.Watch.Roundup
+import Darcs.Watch.Mail
+import HTML (normalizeAuthor)
 
+newtype BundleListMap = BundleListMap { unBundleListMap :: M.Map BundleList (S.Set BundleHash) }
+
+instance Monoid BundleListMap where
+	mempty = BundleListMap M.empty
+	mappend (BundleListMap m1) (BundleListMap m2) =
+		BundleListMap (M.unionWith (S.union) m1 m2)
+
+addBundleListEntry bl bh =
+	tell (mempty,(BundleListMap (M.singleton bl (S.singleton bh))))
+
+addNameOccurence name =
+	tell (Endo (M.insertWith (maxBy length) (normalizeAuthor name) name), mempty)
+
+forBundleListMap :: Monad m => BundleListMap -> (BundleList -> [BundleHash] -> m ()) -> m ()
+forBundleListMap (BundleListMap m) act = mapM_ (\(bl,bhs) -> act bl (S.toList bhs)) $ M.toList m
+
+bundleListMapKeys (BundleListMap m) = M.keys m
+
 updateRepoData config = do
 	let readRepo rep = do
 		inv <- readRepository (cData config)  rep
@@ -55,27 +76,27 @@
 	repos <- mapM readRepo (cRepositories config)
 
 	bundleHashes <- listBundles (cData config)
-	forM_ bundleHashes $ \bundleHash -> do
-		bundle <- getBundle (cData config) bundleHash
-		history <- getBundleHistory (cData config) bundleHash
+	putStrLn "Detecing bundle state changes..."
+	(nameSetters, bundleHashLists) <- execWriterT $ forM_ bundleHashes $ \bundleHash -> do
+		bundle  <- liftIO $ getBundle (cData config) bundleHash
+		history <- liftIO $ getBundleHistory (cData config) bundleHash
 
 		let context = snd bundle
 		let patches = map fst (fst bundle)
-		
+
+		forM patches $ \pi -> do
+			let email = normalizeAuthor (B.unpack (piAuthor pi))
+			addBundleListEntry (AuthorBundleList email) bundleHash
+			addNameOccurence (B.unpack (piAuthor pi))
+
 		forM repos $ \(repo, inv) -> do
-			let statusQuoAnte =
-				fromMaybe New $
-				listToMaybe $
-				map (\(_,_,s) -> s) $
-				filter (aboutRepo repo) $
-				history
-			
-			let statusQuo = 
+			let statusQuoAnte = stateOfRepo history repo
+			    statusQuo = 
 				if all (`S.member` inv) patches then Applied
 				else if applicable inv context then Applicable
 				else New
 
-			when (statusQuo /= statusQuoAnte) $ do
+			when (statusQuo /= statusQuoAnte) $ liftIO $ do
 				putStrLn $ "Marking bundle " ++ bundleHash ++
 			  		  " as " ++ show statusQuo ++
 					  " with regard to " ++ repo
@@ -87,9 +108,22 @@
 				case haveRoundupURL history of
 					Nothing -> return ()
 					Just url -> 
-					  	tellRoundup config url repo bundle statusQuo
+					  	tellRoundup config url repo bundleHash bundle statusQuo
+				
+				mailSubscriptions config repo bundleHash bundle history statusQuo
 
+			when (statusQuo /= New) $
+				addBundleListEntry (RepositoryBundleList repo) bundleHash
 
+	
+	putStrLn $ "Writing bundle lists..."
+	forBundleListMap bundleHashLists $ \bl bh -> do
+		writeBundleList (cData config) bl bh	
+	setBundleListList (cData config) (bundleListMapKeys bundleHashLists)
+
+	putStrLn $ "Writing name mapping list..."
+	writeNameMapping (cData config) (appEndo nameSetters M.empty)
+
 -- Clonsider patches as applicable to a repository when either
 --  * all its context is in the repository
 --  * at least 10 patches of the context are in the repository
@@ -99,5 +133,4 @@
 		(m,_) | length m >= 10 -> True
 		_                      -> False
 
-aboutRepo repo (_,ViaRepository r,_) = repo == r
-aboutRepo _    _                     = False
+maxBy f v1 v2 = if f v1 >= f v2 then v1 else v2
diff --git a/src/Generate-Main.hs b/src/Generate-Main.hs
--- a/src/Generate-Main.hs
+++ b/src/Generate-Main.hs
@@ -60,4 +60,4 @@
         putStrLn "Reading configuration..."
         config <- read `fmap` readFile (confdir ++ "config")
 
-	lockRestart (cOutput config) patchNew or True (generateOutput config)
+	lockRestart (cData config) patchNew or True (generateOutput config)
diff --git a/src/HTML.hs b/src/HTML.hs
--- a/src/HTML.hs
+++ b/src/HTML.hs
@@ -18,16 +18,19 @@
 -}
 
 module HTML 
-	( ResultData(ResultData)
-	, PatchExtras(..)
+	( BundleInfo(..)
 	, mainPage
 	, userPage
 	, repoPage
-	, unmatchedPage
+	, cgiMessagePage
 	, userFile
 	, repoFile
 	, patchDiffFile
 	, normalizeAuthor
+	, bundleInfoFilter
+	, maxState
+	, bundleURL
+	, stripIgnorethis
 	) where
 
 import Text.XHtml hiding ((!))
@@ -58,32 +61,30 @@
 on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
 (*) `on` f = \x y -> f x * f y
 
-data ResultData = ResultData
-	{ p2r ::  M.Map PatchInfo     (S.Set RepositoryURL)
-	, r2p ::  M.Map RepositoryURL (S.Set PatchInfo)
-	, u2p ::  M.Map ByteString    (S.Set PatchInfo)
-	, p2pe::  M.Map PatchInfo     (PatchExtras)
-	, p2pr :: M.Map PatchInfo     (S.Set String)
-	, r2mp :: M.Map RepositoryURL (S.Set PatchInfo)
-	, r2ri :: M.Map RepositoryURL (RepositoryInfo)
-	, unmatched :: (S.Set PatchInfo)
-	, date :: CalendarTime
-	, u2rn :: M.Map ByteString  ByteString
-	} deriving (Show)
-
-data PatchExtras = PatchExtras
-	{ peDiff :: ByteString
-	, peContext :: [PatchInfo]
-	, peBundleFile :: FilePath
-	, peStateHistory :: [BundleHistory]
-	} deriving (Show)
+data BundleInfo = BundleInfo
+	{ biBundleHash :: BundleHash
+	, biBundle :: PatchBundle
+	, biFileName :: FilePath
+	, biHistory :: [BundleHistory]
+	}
 
+instance Eq BundleInfo where
+	(==) = (==) `on` biBundleHash
 
-users d = M.keys (u2p d)
-repos d = M.keys (r2p d)
+instance Ord BundleInfo where
+	compare = compare `on` (map (piDate.fst) . fst . biBundle)
 
-mainPage d = showHtml $
-   header << thetitle << "DarcsWatch overview" +++
+mainPage date
+	 nameMapping
+         patchCount
+	 bundleCount
+	 repoData
+	 authorData
+	= showHtml $
+   header << (
+   	thetitle << "DarcsWatch overview" +++
+	myHeader 
+	) +++
    body << (
 	h1 << "DarcsWatch overview" +++
 	p << ( "Welcome to DarcsWatch. If you want to know more, please read the " +++
@@ -92,31 +93,45 @@
 	       ) +++
 	h2 << "Statistics" +++
 	p << stringToHtml (
-		printf "Tracking %d repositories and %d patches submitted by %d users"
-		(M.size (r2p d)) (M.size (p2pe d)) (M.size (u2p d))
+		printf "Tracking %d repositories and %d patches in %d bundles submitted by %d users."
+		(length repoData) (patchCount :: Int) (bundleCount :: Int) (length authorData)
 		) +++
 	h2 << "Listing by user" +++ 
-	unordList (map (\u -> hotlink (userFile u) << u2rn d ! u +++ userStats u d) (users d)) +++
+	unordList (map userLine authorData) +++
 	h2 << "Listing by repository" +++ 
-	unordList ( map (\r -> hotlink (repoFile r) << r +++ repoStats r d) (repos d)
-			++ [hotlink "unmatched.html" << "Unmatched patches"
-			    +++ " "+++  show (S.size (unmatched d)) +++ " patches" ]
-		  ) +++
-	footer d
+	unordList (map repoLine repoData) +++
+	footer (Just date)
 	)
+  where userLine (u, tracked, unapplied, obsolete, rejected) =
+ 		hotlink (userFile u) << M.findWithDefault u u nameMapping +++
+		" " ++
+		show tracked +++ 
+		" tracked patches, "+++
+		count Applicable "unapplied" unapplied +++
+		count Obsoleted "obsolete" obsolete +++
+		count Rejected "rejected" rejected
+        repoLine (r, inventory, tracked, applicable, obsolete, rejected) =
+ 		hotlink (repoFile r) << r +++
+		" " ++
+		show tracked +++ 
+		" patches in inventory, "+++
+		count Applied "tracked" tracked +++
+		count Applicable "applicable" applicable +++
+		count Obsoleted "obsolete" obsolete +++
+		count Rejected "rejected" rejected
+	count s t 0 = noHtml
+	count s t n = inColor (stateColor s) << (show n +++ " " +++ t) +++ " "
 
-footer d = 
+footer mDate = 
 	p !!! [thestyle "font-size:80%"] << (
 		"darcswatch © Joachim Breitner <" +++
 		hotlink "mailto:mail@joachim-breitner.de" << "mail@joachim-breitner.de" +++
 		">. Source code at " +++
 		hotlink "http://darcs.nomeata.de/darcswatch/" << "http://darcs.nomeata.de/darcswatch/"+++
-		". Last update " +++
-		calendarTimeToString (date d) +++
-		"."
+		"." +++ maybe noHtml (\d -> " Last update " +++ calendarTimeToString d +++ ".") mDate
 		)
 
-myHeader d = script !!! [thetype "text/javascript", src "/javascript/jquery/jquery.js"] << noHtml
+myHeader   = script !!! [thetype "text/javascript", src "/javascript/jquery/jquery.js"] << noHtml
              +++
              script !!! [thetype "text/javascript"] << "\
 		\$(document).ready(function () { \
@@ -140,68 +155,88 @@
 	     	\span[title] { border-bottom: dotted 1px black; } \
 		\"
 
-
-userPage d u = showHtml $
+userPage date nameMapping u bundleInfos = showHtml $
    header << (
-   	thetitle << ("DarcsWatch overview for " +++ u2rn d ! u) +++
-	myHeader d
+   	thetitle << ("DarcsWatch overview for " +++ u) +++
+	myHeader
 	) +++
-   body << (
-	h1 << ("DarcsWatch overview for " +++ u2rn d ! u) +++
+   body << form !!! [ method "GET", action "cgi"] << (
+	h1 << ("DarcsWatch overview for " +++ M.findWithDefault u u nameMapping) +++
 	p << hotlink "." << "Return to main page" +++
-	patchList d (sps !!!! Applicable) "Unapplied patches" True +++
-	patchList d (sps !!!! New) "Unmatched patches" True +++
-	patchList d (sps !!!! Rejected) "Rejected patches" True +++
-	patchList d (sps !!!! Obsoleted) "Obsoleted patches" True +++
-	patchList d (sps !!!! Applied) "Applied patches" True +++
-	footer d
+	bundleList nameMapping "Unapplied patch bundles" (bundleInfoFilter Applicable bundleInfos) +++ 
+	bundleList nameMapping "Unmatched patch bundles" (bundleInfoFilter New bundleInfos) +++ 
+	bundleList nameMapping "Applied patch bundles" (bundleInfoFilter Applied bundleInfos) +++ 
+	bundleList nameMapping "Obsoleted patch bundles" (bundleInfoFilter Obsoleted bundleInfos) +++ 
+	bundleList nameMapping "Rejected patch bundles" (bundleInfoFilter Rejected bundleInfos) +++ 
+	footer (Just date)
 	)
-  where (ps, sps) = userData u d
 
-repoPage d r = showHtml $
+
+repoPage date nameMapping r repoInfo bundleInfos = showHtml $
    header << (
    	thetitle << ("DarcsWatch overview for " +++ r) +++
-	myHeader d
+	myHeader
 	) +++
-   body << (
+   body << form !!! [ method "GET", action "cgi"] << (
 	h1 << ("DarcsWatch overview for " +++ r) +++
 	p << hotlink "." << "Return to main page" +++
-	patchList d (sps !!!! Applicable) "Unapplied patches" False +++
-	patchList d (sps !!!! Applied) "Applied patches" False +++
-	patchList d (sps !!!! Obsoleted) "Obsoleted patches" False +++
-	patchList d (sps !!!! Rejected) "Rejected patches" True +++
+	bundleList nameMapping "Unapplied patch bundles" (bundleInfoFilter Applicable bundleInfos) +++ 
+	bundleList nameMapping "Applied patch bundles" (bundleInfoFilter Applied bundleInfos) +++ 
+	bundleList nameMapping "Obsoleted patch bundles" (bundleInfoFilter Obsoleted bundleInfos) +++ 
+	bundleList nameMapping "Rejected patch bundles" (bundleInfoFilter Rejected bundleInfos) +++ 
 	thediv << (
 		"Last change in repository at " +++
-		maybe (toHtml "No idea when") toHtml (lastUpdate (r2ri d ! r)) +++
+		maybe (toHtml "No idea when") toHtml (lastUpdate repoInfo) +++
 		", last check of repository at " +++
-		maybe (toHtml "No idea when") toHtml (lastCheck (r2ri d ! r)) +++ "."
+		maybe (toHtml "No idea when") toHtml (lastCheck repoInfo) +++ "."
 	) +++
-	footer d
+	footer (Just date)
 	)
-  where (ps, sps) = repoData r d
- 
+
+bundleList nameMapping title [] = noHtml
+bundleList nameMapping title list = 
+	h2 << title +++
+	unordList (map (bundleView nameMapping) list)
+
+bundleInfoFilter state = sort . filter (\bi -> state == maxState (biHistory bi))
+
+{-
 unmatchedPage d = showHtml $
    header << (
    	thetitle << ("DarcsWatch overview, unmatched patches") +++
-	myHeader d
+	myHeader
 	) +++
-   body << (
+   body << form !!! [ method "GET", action "cgi"] << (
 	h1 << ("DarcsWatch overview, unmatched Patches") +++
 	p << hotlink "." << "Return to main page" +++
 	patchList d (S.toList (unmatched d)) "Unmatched patches" False +++
-	footer d
+	footer (Just (date d))
 	)
+-}
 
-patchList d [] title userCentric = h5 << ("No "++title)
-patchList d ps title userCentric = 
-	h2 << title +++ (unordList $ map (patchView d userCentric) ps)
+cgiMessagePage isError msg = showHtml $
+   header << (
+   	thetitle << "DarcsWatch" +++
+	myHeader
+	) +++
+   body << (
+	h1 << "DarcsWatch" +++
+	p << (
+		(if isError then inColor "red" << "Error: " else noHtml)
+		+++
+		msg
+	      ) +++
+	footer Nothing
+	)
 
-patchHistoryView d p = 
- 	unordList $ flip map (reverse $ peStateHistory $ p2pe d ! p) $ \(date,source,state) ->
-			date +++ ": " +++ showState state +++ " " +++ showSource source
+patchHistoryView history = unordList $
+	map (\(date,source,state) ->
+		date +++ ": " +++ showState state +++ " " +++ showSource source)
+	    (reverse history)
 
 showSource (ViaRepository repo) = " in repo " +++ hotlink (repoFile repo) << repo
 showSource (ViaBugtracker url) = " in bug tracker ticket " +++ hotlink url << url
+showSource (ViaWeb openid) = " via web interface by user " +++ hotlink openid << openid
 showSource (ViaEMail from to subject mmid) = " via " +++
 					     thespan !!! [ title $
 					     	"From: " ++ from ++ "\n" ++
@@ -219,44 +254,72 @@
 
 showSource ManualImport = toHtml "via a manual import"
 
-patchView d userCentric p =
+bundleView nameMapping (BundleInfo bundleHash (ps,ctx) bundleFileName history) = 
+	p << (anchor !!! [name ("bundle-" ++ bundleHash)] << noHtml +++
+	     strong << "Contents:" +++
+	     " (" +++
+	     (if length ps == 1
+	      then "1 patch"
+	      else show (length ps) ++ " patches" ) +++
+	     ")"
+	) +++
+	unordList (map (patchView nameMapping) (map fst ps)) +++
+	p << (strong << "History:") +++
+	patchHistoryView history +++
+	actions
+  where
+	actions = paragraph << (
+			strong << "Actions: " +++
+			hotlink bundleFileName << "Download .dpatch" +++
+			" "+++ 
+			if maxState history <= Applicable
+			then	" "+++
+				select !!! [ name ("state-" ++ bundleHash), size "1"] << (
+					option !!! [ value "UNCHANGED" ] << "Mark patch as..." +++
+					option !!! [ value "OBSOLETE" ] << "Obsolete" +++
+					option !!! [ value "REJECTED" ] << "Rejected"
+					) +++
+				submit "submit" "Submit"
+			else noHtml
+			)
+
+patchView nameMapping p =
 	piDate p +++ ": " +++ strong << (
 		(if piInverted p then stringToHtml "UNDO: " else noHtml) +++
 		piName p
 		) +++
-	(if userCentric
-	 then	noHtml
-	 else   (	" " +++ small << (
-	 	      " by " +++ hotlink (userFile (piAuthor p)) << piAuthor p
-		      )
-	 	)
+	" " +++
+	small << (
+		" by " +++
+		let u = B.unpack (piAuthor p) in
+		hotlink (userFile u) <<
+			M.findWithDefault u u nameMapping +++
+		" " +++
+		diffShower
 	) +++
-	pre << B.unlines (piLog p) +++
-	patchHistoryView d p +++
-	actions +++
+	(if null log then noHtml else pre << B.unlines log) +++
 	thediv !!! [identifier diffId, thestyle "display:none"] << pre noHtml
   where pid = patchBasename p
 	diffShowerId = "diffshower_"++pid
 	diffId = "diff_"++pid
-	actions = paragraph << (
-			strong << "Actions: " +++
-			hotlink (pid ++ ".dpatch") << "Download .dpatch" +++
-			" "+++ 
-			anchor !!! [identifier diffShowerId, theclass "diffshower", href "javascript:"]
-				<< "Show/Hide diff"
-			)
-		
+	diffShower = anchor
+		!!! [identifier diffShowerId, theclass "diffshower", href "javascript:"]
+		<< "Show/Hide diff"
+	log  = stripIgnorethis (piLog p)
+
+maxState history = maximum $ New : map (\(_,_,s) -> s) history
+
+{-
 state d p r | p `S.member` ps                          = Applied
-            | ip `S.member` ps                         = explicit_state
+            | ip `S.member` ps                         = maxState history
 	    | amend_obsoleted                          = Obsoleted
             | ip `S.member` subs && not (piInverted p) = Obsoleted
-            | otherwise                                = explicit_state
+            | otherwise                                = maxState history
   where context = peContext (p2pe d ! p)
 	history = peStateHistory (p2pe d ! p)
   	ps = r2p d ! r
         ip = inversePatch p
 	subs = M.keysSet (p2pe d)
-	explicit_state = maximum $ New : map (\(_,_,s) -> s) history
 	amend_obsoleted = any (`laterThan` p) $ S.toList (u2p d ! (normalizeAuthor (piAuthor p)))
 
 p1 `laterThan` p2 =    piAuthor p1 == piAuthor p2
@@ -264,6 +327,7 @@
 		    && piLog p1    == piLog p2
 		    && piInverted p1 == piInverted p2
 		    && piDate p1   > piDate p2
+-}
 
 showState New = "Seen bundle"
 showState Applicable = "Not yet fully applied bundle"
@@ -279,13 +343,10 @@
 
 inColor c = thespan !!! [thestyle ("color:"++c)]
 
-viewState d p r = inColor (stateColor s) << (showState s)
-  where	s = state d p r
-
 (!!!) :: ADDATTRS a => a -> [HtmlAttr] -> a
 (!!!) = (Text.XHtml.!)	    
 
-
+{-
 userStats u d = " " +++
 	show (length ps) +++ 
 	" tracked patches, "+++
@@ -321,19 +382,17 @@
 	state' p | S.null repos = New
 	         | otherwise    = S.findMax (S.map (state d p) repos)
 	  where repos = p2pr d !!!! p
+-}
 
-userFile u = "user_" ++ B.unpack (normalizeAuthor u) ++ ".html"
+userFile u = "user_" ++ normalizeAuthor u ++ ".html"
 repoFile r = "repo_" ++ safeName r ++ ".html"
 patchDiffFile p = "diff_" ++ patchBasename p ++ ".txt"
 
-normalizeAuthor name | not (B.null r') && valid = email
-                     | otherwise                = safeNameB name
-  where r' = B.dropWhile (/='<') name
-        (email,r'') = B.span (/='>') (B.tail r')
-	valid = not (B.null email) && not (B.null r'') && B.all (isSafeFileChar) email
-
-safeNameB n = B.map s n
-  where s c = if isSafeFileChar c then c else '_'
+normalizeAuthor name | not (null r') && valid = email
+                     | otherwise              = safeName name
+  where r' = dropWhile (/='<') name
+        (email,r'') = span (/='>') (tail r')
+	valid = not (null email) && not (null r'') && all (isSafeFileChar) email
 
 safeName n = map s n
   where s c = if isSafeFileChar c then c else '_'
@@ -344,8 +403,19 @@
 s2List = patchSort . S.toList
 patchSort = sortBy (flip (compare `on` piDate))
 
+bundleURL :: DarcsWatchConfig -> RepositoryURL -> BundleHash -> String
+bundleURL config repo hash = cDarcsWatchURL config ++ repoFile repo ++ "#bundle-" ++ hash
+
+stripIgnorethis [] = []
+stripIgnorethis (x:xs) | B.pack "Ignore-this:" `B.isPrefixOf` x = stripIgnorethis xs
+		       | B.null x                               = stripIgnorethis xs
+		       | otherwise                              = x:xs
+
 instance HTML ByteString where
 	toHtml = toHtml . B.unpack
 
 instance HTML UTCTime where
+	toHtml = toHtml . formatTime defaultTimeLocale "%c" 
+
+instance HTML ZonedTime where
 	toHtml = toHtml . formatTime defaultTimeLocale "%c" 
diff --git a/src/PullRepos-Main.hs b/src/PullRepos-Main.hs
--- a/src/PullRepos-Main.hs
+++ b/src/PullRepos-Main.hs
@@ -58,7 +58,6 @@
         putStrLn "Reading configuration..."
         config <- read `fmap` readFile (confdir </> "config")
 
-	--lockRestart (cOutput config) patchNew or True (do_work config)
 	new <- pullRepos config
 	when new $ do
 		lockRestart (cData config) False or True $ \new -> do
diff --git a/src/UpdateRepoData-Main.hs b/src/UpdateRepoData-Main.hs
--- a/src/UpdateRepoData-Main.hs
+++ b/src/UpdateRepoData-Main.hs
@@ -57,5 +57,4 @@
         putStrLn "Reading configuration..."
         config <- read `fmap` readFile (confdir </> "config")
 
-	--lockRestart (cOutput config) patchNew or True (do_work config)
 	updateRepoData config
