Browse Source

Add access log feature to admin page. Mostly backend work

Sebastian Kreisel 1 year ago
parent
commit
ca050a74f3

+ 7 - 1
docker/Dockerfile_run

@@ -5,14 +5,20 @@ ARG elfcomexe
 RUN mkdir -p /var/elfcom/public/static/js && \
     mkdir -p /var/elfcom/private/
 
+# Add robots.txt
+COPY elfcom-git/elfcom-frontend/static/robots.txt \
+     /var/elfcom/public/static/robots.txt
+
 # Add css and img
 COPY elfcom-git/elfcom-backend/static/css /var/elfcom/public/static/css
 COPY elfcom-git/elfcom-backend/static/img /var/elfcom/public/static/img
 
 # Add elfeck-frontend
-COPY elfcom-git/elfcom-frontend/common.js /var/elfcom/public/static/js/common.js
+COPY elfcom-git/elfcom-frontend/common.js \
+     /var/elfcom/public/static/js/common.js
 COPY elfcom-git/elfcom-frontend/edit /var/elfcom/public/static/js/edit
 COPY elfcom-git/elfcom-frontend/files /var/elfcom/public/static/js/files
+COPY elfcom-git/elfcom-frontend/admin /var/elfcom/public/static/js/admin
 
 # Add parseck_js
 COPY parts/parseck_js/src/parseck.js /var/elfcom/public/static/js/parseck.js

+ 1 - 1
elfcom-backend/config/deploy.conf

@@ -2,7 +2,7 @@
 deploy
 
 [Session]
-1 31557600,5 64800
+1 31557600,5 259200
 
 [RootDir]
 /var/elfcom/public

+ 1 - 1
elfcom-backend/config/dev.conf

@@ -2,7 +2,7 @@
 development
 
 [Session]
-1 31557600,5 64800
+1 31557600,5 315576001
 
 [RootDir]
 .

+ 0 - 37
elfcom-backend/converted.txt

@@ -1,37 +0,0 @@
-Jaja. Gerade war ich bei der dritten Swing-out Stunde und das war eine ziemliche Katastrophe. Sehr frustrierend!
-
-
-Seit Anfang dieses Zyklus schon hab ich Schwierigkeiten. Und woran liegts? Spannung und Körperhaltung. Und wenn das nicht klappt, was es nicht tut, dann klappen auch keine komplizierteren Figuren. Und man fühlt sich schlecht weil der Follower dann auch zu nichts kommt. Goddamn.
-
-
-Swing-out gilt nicht ohne Grund als eine der schwierigsten Figuren, wenn auch elementar.
-
-
-Soviel Dinge die man richtig machen soll und muss, so viele die ich nicht sauber hinbekommen.
-
-
-\begin{enumerate}
-\item Spannung
-
-\item Haltung und Rahmen
-
-\item Saubere Schritte
-
-\item Den linken Arm nicht nach hinten sondern der soll beim drehen helfen
-
-\item Kleine Schritte und nahe zusammen
-
-\item Figur führen 
-
-\item Spannung
-
-\item Spannung und Haltung
-
-\item Spannung und Haltung und Rahnen
-
-\end{enumerate}
-Dann kommt noch die schnelle Musik dazu und ein Follower der auch nicht alles perfekt macht.
-
-
-Richtig schwer. Und sehr frustrierend besonders wenn man jedes mal seinen Fehler bemerkt aber nicht verbessern kann. Aarrrrrg. AAARrrg. Fuck. Gedult. Gedult.
-

+ 1 - 0
elfcom-backend/elfcom-backend.cabal

@@ -40,6 +40,7 @@ executable elfcom-backend-exe
                      , Endpoint.Edit
                      , Endpoint.Files
                      , Endpoint.Login
+                     , Endpoint.AccessLog
   build-depends:       base >= 4.7 && < 5
                      , Spock
                      , hvect

+ 5 - 2
elfcom-backend/src/Common.hs

@@ -12,6 +12,7 @@ import Network.HTTP.Types.Status
 
 import System.Random
 import Control.Concurrent.STM.TChan
+import Control.Concurrent.STM.TVar
 import Control.Monad.STM
 import Control.Monad.Trans
 import Control.Monad.Logger
@@ -41,6 +42,7 @@ import View.Error
 data AppState = AppState { elfeckPerm :: [T.Text]
                          , elfeckPermLength :: Int
                          , accessLogChan :: TChan AccessLogEntry
+                         , accessLogLock :: TVar Bool
                          , developMode :: Bool
                          }
 type SessionVal = Maybe SessionId
@@ -236,7 +238,7 @@ randomInt l u = do
   getStdRandom (randomR (l, u))
 
 getRandomElf :: AppState -> Int -> IO T.Text
-getRandomElf (AppState perms l _ _) n = do
+getRandomElf (AppState perms l _ _ _) n = do
   _ <- newStdGen
   inds <- mapM (\_ -> randomInt 0 (l - 1)) [1..n] --n-2]
   return $ T.intercalate " " (map (\i -> perms !! i) inds)
@@ -284,8 +286,9 @@ spockConfig (SiteConfig dev sessDurs _ _ _ _) conn = do
   inds <- mapM (\_ -> randomInt 0 (l - 1)) [1..sell]
   let selperms = map (\i -> perms !! i) inds
   accessChan <- atomically newTChan
+  accessLock <- atomically $ newTVar True
   return SpockCfg
-    { spc_initialState = AppState selperms sell accessChan dev
+    { spc_initialState = AppState selperms sell accessChan accessLock dev
     , spc_database = conn
     , spc_sessionCfg = sessConfig
     , spc_maxRequestSize = Just (5 * 1024 * 1024)

+ 47 - 0
elfcom-backend/src/Endpoint/AccessLog.hs

@@ -0,0 +1,47 @@
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE DeriveGeneric  #-}
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Endpoint.AccessLog where
+
+import Control.Monad.Trans
+import Data.Time.Format
+import Data.Time
+import Data.Aeson hiding (json)
+import Data.HVect hiding (tail)
+import qualified Data.Text as T
+import Web.Spock hiding (head, SessionId)
+import Control.Concurrent.STM.TVar
+import GHC.Generics
+
+import Common
+import Model.AccessLog
+
+handleAccessLogEndpoints :: ListContains n IsAdmin xs => TVar Bool -> T.Text ->
+                            App (HVect xs)
+handleAccessLogEndpoints lock logD = do
+  post (baseRoute <//> "fetch") $ connectH (accessLogFetchHandler lock logD)
+  where baseRoute = "api" <//> "admin" <//> "accesslog"
+
+accessLogFetchHandler :: TVar Bool -> T.Text -> AccessLogFetchReq ->
+                         Action ctx AccessLogFetchRsp
+accessLogFetchHandler lock logD (AccessLogFetchReq mself mrobots mtime mip) =do
+  fileContents <- liftIO $ readAccessLogFiles lock logD mtime
+  return $ AccessLogFetchOkay fileContents
+
+
+data AccessLogFetchReq = AccessLogFetchReq {
+  fetMe :: !Bool
+  , fetRobots :: !Bool
+  , fetTime :: !(Maybe (UTCTime, UTCTime))
+  , fetIps :: !(Maybe [T.Text]) }
+                       deriving (Show, Eq, Generic, ToJSON, FromJSON)
+
+data AccessLogFetchRsp = AccessLogFetchOkay {
+--  fetContent :: ![AccessLogSummary] }
+  fetContent :: !T.Text }
+                       | AccessLogFetchFail { fetError :: !T.Text }
+                       deriving (Show, Eq, Generic, ToJSON, FromJSON)

+ 3 - 1
elfcom-backend/src/Main.hs

@@ -41,6 +41,7 @@ import View.Post
 import Endpoint.Edit
 import Endpoint.Login
 import Endpoint.Files
+import Endpoint.AccessLog
 
 
 -- Main
@@ -122,6 +123,7 @@ app (SiteConfig isDev sessDur _ _ filesD logD) = do
     prehook (authHook isDev) $ prehook (adminHook isDev) $ do
       handleEditEndpoints
       handleFilesEndpoints filesD
+      handleAccessLogEndpoints (accessLogLock appState) logD
 
     -- API NOT FOUND
     hookAny POST (\t -> notfoundError (T.append "/" (T.intercalate "/" t)))
@@ -265,7 +267,7 @@ ipHook qWorker = do
                                   ipAddr
                                   (foo . T.pack $ show p)
                                   (fmap (foo . T.pack . show) ref)
-                                  (fmap fst mu)
+                                  (fmap (fromIntegral . fromSqlKey . fst) mu)
                                   IpUnset)
                  qWorker)
   getContext

+ 141 - 27
elfcom-backend/src/Model/AccessLog.hs

@@ -1,52 +1,63 @@
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE DeriveGeneric  #-}
+{-# LANGUAGE DeriveAnyClass #-}
+
 module Model.AccessLog where
 
 import Data.Time
 import Data.Maybe
 import Data.List
+import Text.Read (readMaybe)
 import Control.Concurrent.STM.TChan
+import Control.Concurrent.STM.TVar
 import Control.Monad.STM
 import qualified Data.Text as T
-import Database.Persist.Sqlite (fromSqlKey)
+import qualified Data.Text.IO as TIO (readFile)
 import Data.Aeson
 import Network.HTTP
 import Data.ByteString.Lazy.Char8 (pack)
+import GHC.Generics
 
 import Model.DBTypes
 
 
-data IpLocation = IpUnset |
-                  IpConnectionError |
-                  IpRateError |
-                  IpServerError |
-                  IpJSONError |
-                  IpLocation { country :: T.Text
-                             , countryCode :: T.Text
-                             , region :: T.Text
-                             , city :: T.Text
-                             , lat :: Double
-                             , lon :: Double
-                             }
-                deriving Show
+data IpLocation = IpUnset | IpConnectionError | IpRateError | IpServerError |
+                  IpJSONError | IpLocation { country :: T.Text
+                                           , countryCode :: T.Text
+                                           , region :: T.Text
+                                           , city :: T.Text
+                                           , lat :: Double
+                                           , lon :: Double }
+                deriving (Show, Eq, Generic, ToJSON)
 
 instance FromJSON IpLocation where
-  parseJSON (Object v) =
-    IpLocation <$> v .: "country_name"
-    <*> v .: "country_code"
-    <*> v .: "region_name"
-    <*> v .: "city"
-    <*> v .: "latitude"
-    <*> v .: "longitude"
+  parseJSON (Object v) = IpLocation <$> v .: "country_name"
+                         <*> v .: "country_code" <*> v .: "region_name"
+                         <*> v .: "city" <*> v .: "latitude"
+                         <*> v .: "longitude"
 
 
 data AccessLogEntry = AccessLogEntry { timestamp :: UTCTime
                                      , ip :: T.Text
                                      , path :: T.Text
                                      , referer :: Maybe T.Text
-                                     , userId :: Maybe UserId
-                                     , loc :: IpLocation
-                                     }
+                                     , userId :: Maybe Int
+                                     , loc :: IpLocation }
                     deriving Show
 
+data AccessLogSummary = IpSummary {
+  sumIp :: T.Text
+  , sumUserIds :: [Int]
+  , sumIsRobot :: Bool
+  , sumLocs :: [IpLocation]
+  , accessLine :: [(UTCTime, T.Text, T.Text)] }
+                      deriving (Show, Eq, Generic, ToJSON, FromJSON)
+
+-- -------------------------------------------------------------------------
+
 queueAccessLogEntry :: TChan AccessLogEntry -> AccessLogEntry -> IO ()
 queueAccessLogEntry chan logEntry =
   atomically $ writeTChan chan logEntry
@@ -94,22 +105,125 @@ requestIpLoc ipp = do
 
 -- --------------------------------------------------------------------------
 
-writeToAccessLog :: TChan AccessLogEntry -> T.Text -> IO ()
-writeToAccessLog chan logDir = do
+writeToAccessLog :: TChan AccessLogEntry -> TVar Bool -> T.Text -> IO ()
+writeToAccessLog chan lockVar logDir = do
   now <- getCurrentTime
   let ts = formatTime defaultTimeLocale "%Y-%m" now
   let logFile = T.unpack logDir ++ "/accesslog_" ++ ts ++ ".txt"
   entries <- atomically $ collectFromChannel chan []
   procEntries <- setIpLocations entries
   let lines = map buildLogLine procEntries
+  atomically $ do
+    lock <- readTVar lockVar
+    case lock of
+      True -> writeTVar lockVar False
+      False -> retry
+  --putStrLn "W+A: Writer has aquired lock ..."
   appendFile logFile (T.unpack $ T.unlines lines)
+  atomically $ writeTVar lockVar True
+  --putStrLn "W+R: Writer has released lock ..."
 
 buildLogLine :: AccessLogEntry -> T.Text
 buildLogLine (AccessLogEntry t ipaddr p ref uid ipLoc) =
   let ts = T.pack $ formatTime defaultTimeLocale "%F_%T" t
   in T.intercalate "," ([ts, ipaddr, p, fromMaybe "" ref,
-                        T.pack $ fromMaybe "" (fmap (show . fromSqlKey) uid)]
+                        T.pack $ fromMaybe "" (fmap show uid)]
                         ++ foo ipLoc)
   where foo (IpLocation c cc r ci la lo) = [c, cc, r, ci, T.pack $ show la,
                                             T.pack $ show lo]
         foo l = [T.pack $ show l]
+
+-- -------------------------------------------------------------------------
+
+readAccessLogFiles :: TVar Bool -> T.Text -> Maybe (UTCTime, UTCTime)
+                      -> IO T.Text
+readAccessLogFiles lockVar logDir mtime = do
+  atomically $ do
+    lock <- readTVar lockVar
+    case lock of
+      True -> writeTVar lockVar False
+      False -> retry
+  --
+  --putStrLn "R+A: Read has aquired lock ..."
+  now <- getCurrentTime
+  let (from, to) = fromMaybe (now, now) mtime
+  let fromS = dateTuplify (words $ formatTime defaultTimeLocale "%Y %m" from)
+  let toS = dateTuplify (words $ formatTime defaultTimeLocale "%Y %m" to)
+  fileList <- case (fromS, toS) of
+    (Just f, Just t) -> return $ map constrFileName (datesBetween f t [])
+    _ -> return []
+  contents <- mapM TIO.readFile fileList
+  --
+  atomically $ writeTVar lockVar True
+  --putStrLn "R+R: Read has released lock ..."
+  return $ T.concat contents
+  where constrFileName (y, m) = concat [T.unpack logDir, "/accesslog_",
+                                        show y, "-", appZ m, ".txt"]
+        appZ mm | mm < 10 = "0" ++ (show mm)
+                | otherwise = show mm
+
+dateTuplify :: [String] -> Maybe (Int, Int)
+dateTuplify (a : b : []) = case catMaybes (map readMaybe [a, b]) of
+  (a : b : []) -> Just (a, b)
+  _ -> Nothing
+dateTuplify _ = Nothing
+
+datesBetween :: (Int, Int) -> (Int, Int) -> [(Int, Int)] -> [(Int, Int)]
+datesBetween (fy, fm) (ty, tm) acc = case dateLessThan (fy, fm) (ty, tm) of
+  False -> acc ++ [(fy, fm)]
+  True -> datesBetween (fy + (quot fm 12), foo $ (fm + 1) `mod` 13) (ty, tm)
+          (acc ++ [(fy, fm)])
+    where foo 0 = 1
+          foo x = x
+
+dateLessThan :: (Int, Int) -> (Int, Int) -> Bool
+dateLessThan (y1, m1) (y2, m2) | y1 < y2 = True
+                               | y1 == y2 = m1 < m2
+                               | otherwise = False
+
+-- -------------------------------------------------------------------------
+
+buildIpSummaries :: Bool -> Bool -> (UTCTime, UTCTime) -> [T.Text] ->
+                    T.Text -> [AccessLogSummary]
+buildIpSummaries me rb (f, t) ips logtext =
+  map buildIpSummary (groupBy (\x y -> (ip x) == (ip y))
+                      (filterRobots me rb (f, t) ips logtext))
+
+buildIpSummary :: [AccessLogEntry] -> AccessLogSummary
+buildIpSummary es = IpSummary (ip $ head es)
+                  (catMaybes $ nub (map userId es))
+                  (not $ null (filter (\e -> path e == "/robots.txt") es))
+                  (nub (map loc es))
+                  (map(\e ->(timestamp e,path e,fromMaybe "" $ referer e)) es)
+
+filterRobots :: Bool -> Bool -> (UTCTime, UTCTime) -> [T.Text] ->
+                T.Text -> [AccessLogEntry]
+filterRobots me rb (f, t) ips logText =
+  filter (\e -> rb && ((ip e) `notElem` robotIps)) entries
+  where entries = catMaybes $
+                  map (\l -> logLineToEntry me rb (f,t) ips (T.splitOn "," l))
+                  (T.lines logText)
+        robotIps = nub $ map ip (filter (\e -> (path e) == "/robots.txt")
+                                 entries)
+
+-- Checks time and ip constraint
+logLineToEntry :: Bool -> Bool -> (UTCTime, UTCTime) -> [T.Text] ->
+                         [T.Text] -> Maybe AccessLogEntry
+logLineToEntry me rb (f, t) ips toks
+  | foo = Just $ AccessLogEntry tim (toks !! 1) (toks !! 2) (ttM $ toks !! 3)
+          (readMaybe $ T.unpack (toks !! 4)) (prsLocation (drop 5 toks))
+  | otherwise = Nothing
+  where tim = (prsTime (toks !! 0))
+        foo = length toks == 11 &&
+              tim >= f && tim <= t &&
+              ((toks !! 1) `notElem` ips)
+        prsTime t = fromMaybe (addUTCTime (-3600) f)
+                    (parseTimeM True defaultTimeLocale "%F_%T"
+                     (T.unpack t))
+        prsLocation (c : cc : r : ci : lon : lat : [])
+          | c == "" && cc == "" = IpUnset
+          | otherwise = IpLocation c cc r ci
+                        (fromMaybe 0 (readMaybe $ T.unpack lat))
+                        (fromMaybe 0 (readMaybe $ T.unpack lon))
+        ttM "" = Nothing
+        ttM t = Just t

+ 6 - 4
elfcom-backend/src/View/Admin.hs

@@ -14,6 +14,8 @@ adminSite mu rurl rndElf = do
   let path = urlToPath rurl
   siteHead path $ do
     makeCss $ T.append path "/css/site_admin.css"
+    makeCss $ T.append path "/css/input.css"
+    makeJs_ $ T.append path "/js/admin/site_admin.js"
   siteBody $ do
     siteHeader
     rootNav rndElf
@@ -23,13 +25,13 @@ adminSite mu rurl rndElf = do
 
 adminMain :: Html
 adminMain = div ! class_ "cnt-admin-main" $ do
-  div ! class_ "cnt-admin-full admin-welcome" $ do
-    "Hello hello. Happy admin'ing. Ah, and have a nice day :)"
   div ! class_ "cnt-admin-left" $ do
     div ! class_ "admin-headline" $ "Actions"
     div ! class_ "admin-entry" $ a ! href "/" $ "» Back Home"
     div ! class_ "admin-entry" $ a ! href "/admin/edit" $ "» Manage Posts"
     div ! class_ "admin-entry" $ a ! href "/admin/files" $ "» Manage Files"
   div ! class_ "cnt-admin-right" $ do
-    div ! class_ "admin-headline" $ "Something something"
-    div ! class_ "admin-placeholder" $ "~ give it some time ~"
+    div ! class_ "admin-log" $ do
+      input ! id "admin-log-search"
+      div ! id "admin-log-content" $ ""
+      input ! id "admin-log-response"

+ 1 - 1
elfcom-backend/src/View/Drivel.hs

@@ -142,7 +142,7 @@ postEntry now (Post title _ url cR _ crtDate _ ptype _ _) = do
          2 -> do
            div ! class_ "drivel-title-cnt" $ do
              div ! class_ "drivel-drivel-title" $
-               toHtml $ (rndDrivel timeDiff) ++ " just floating rubble " ++
+               toHtml $ (rndDrivel timeDiff) ++ " floating rubble " ++
                (rndDrivel (timeDiff - ((timeDiff * 43) / 2) + 42))
              div ! class_ "drivel-date-date" $
                toHtml $ timeString ++ " ago"

+ 1 - 1
elfcom-backend/src/View/Error.hs

@@ -28,7 +28,7 @@ noaccessSite rurl rndElf = errorSite rurl rndElf $ do
     a ! class_ "nav-link" ! href "/" $ "elfeck home"
     "."
   div ! class_ "cnt-error-block cnt-error-small"  $ do
-    i "If you think you should have access to this site, write me an email."
+    i "If you think you should have access to this page, write me an email."
     br
     i "seb (at) elfeck (dot) com"
 

+ 2 - 2
elfcom-backend/src/Worker.hs

@@ -43,8 +43,8 @@ accessLogEnqueueDef = (WorkerDef accessLogEnqueueConfig doAccessLogEnqueue
 doAccessLogWrite :: (SpockState m ~ AppState, HasSpock m, MonadIO m) =>
                     T.Text -> m WorkResult
 doAccessLogWrite logDir = do
-  appState <- getState
-  liftIO $ writeToAccessLog (accessLogChan appState) logDir
+  st <- getState
+  liftIO $ writeToAccessLog (accessLogChan st) (accessLogLock st) logDir
   return $ WorkRepeatIn 10
 
 accessLogWriteConfig :: WorkerConfig

+ 19 - 19
elfcom-backend/static/css/site_admin.css

@@ -2,13 +2,6 @@ div.cnt-admin-main {
     font-family: "DejaVu Serif", "Georgia", serif;
 }
 
-div.cnt-admin-full {
-    margin: 20px;
-    width: 758px;
-    height: 30px;
-    border: 1px grey dotted;
-}
-
 div.cnt-admin-left {
     display: inline-block;
     vertical-align: top;
@@ -24,16 +17,7 @@ div.cnt-admin-right {
     vertical-align: top;
     width: 368px;
     margin: 20px;
-    border: 1px grey dotted;
-    height: 300px;
-}
-
-
-div.admin-welcome {
-    font-size: 14px;
-    text-align: center;
-    padding-top: 12px;
-    margin-bottom: 0px;
+    --border: 1px grey dotted;
 }
 
 div.admin-headline {
@@ -49,7 +33,23 @@ div.admin-entry {
     font-size: 14px;
 }
 
-div.admin-placeholder {
+div.admin-log {
+    width: 368px;
+}
+
+input#admin-log-response {
+    width: 368px;
     text-align: center;
-    margin-top: 100px;
+    margin-top: 0px;
+}
+
+input#admin-log-search {
+    width: 368px;
+    margin-bottom: 0px;
+    font-size: 9px;
+}
+
+div#admin-log-content {
+    border: 1px grey solid;
+    height: 300px;
 }

+ 2 - 0
elfcom-backend/static/robots.txt

@@ -0,0 +1,2 @@
+User-agent: Googlebot-Image
+Disallow: /files/

+ 80 - 0
elfcom-frontend/admin/log.js

@@ -0,0 +1,80 @@
+"use strict";
+
+import Common from "../common.js";
+
+
+var Log = {
+  init: init,
+  doFetch: doFetch,
+};
+export default Log;
+
+function init(sobjs) {
+  // date: h:m d/m/y
+  var start = new Date(); start.setDate(start.getDate() - 1);
+  var end = new Date();
+  function buildDS(d) {
+    return Common.dayString(d) + "/" +
+      Common.monthString(d) + "/" +
+      Common.yearString(d) + " " +
+      Common.hourString(d) + ":" +
+      Common.minuteString(d);
+  }
+  var dateString = "(" + buildDS(start) + " - " + buildDS(end) + ")";
+  sobjs.seaEl.value = "me;rb;date=" + dateString + ";";
+  doFetch(sobjs);
+}
+
+function doFetch(sobjs) {
+  var tokens = sobjs.seaEl.value.split(";");
+  var meToken = Common.filterTokens(tokens, "me");
+  var rbToken = Common.filterTokens(tokens, "rb");
+  var exclIpToken = Common.filterTokens(tokens, "excl");
+  var dateToken = Common.filterTokens(tokens, "date");
+  var dates = null;
+  var ips = null;
+  if(dateToken !== null) {
+    var dateTuple = dateToken.slice(1, dateToken.length - 1).split(" - ");
+    if(dateTuple.length === 2) {
+      dates = [ tokenToDate(dateTuple[0]), tokenToDate(dateTuple[1]) ];
+      if(dates.includes(null)) {
+        dates = null;
+      }
+    }
+  }
+  var me = false;
+  var rb = false;
+  if(meToken !== null) { me = true; }
+  if(rbToken !== null) { rb = true; }
+  var jsonObj = {
+    "fetMe": me,
+    "fetRobots": rb,
+    "fetTime": dates,
+    "fetIps": ips,
+  };
+  return new Promise(function(resolve, reject) {
+    Common.sendJson("/api/admin/accesslog/fetch", jsonObj)
+      .then(res => res.json(),
+            () => { Common.fetchError("Fetch"); reject("ajax failed"); })
+      .then(function(json) {
+        if(json.tag === "AccessLogFetchOkay") {
+          console.log(json.fetContent);
+          resolve();
+        } else {
+          Common.serverError("Fetch"); reject("server failed");
+        }
+      }, () => { Common.jsonError("Fetch"); reject("json failed"); });
+  });
+}
+
+function tokenToDate(tok) {
+  var ts = tok.split(" ").filter(t => t.trim().length > 0);
+  if(ts.length === 2) {
+    var cs = ts[0].split("/").filter(t => t.trim().length > 0);
+    var hs = ts[1].split(":").filter(t => t.trim().length > 0);
+    if(cs.length === 3 && hs.length === 2) {
+      return new Date("20" + cs[2], cs[1] - 1, cs[0], hs[0], hs[1]);
+    }
+  }
+  return null;
+}

+ 27 - 0
elfcom-frontend/admin/site_admin.js

@@ -0,0 +1,27 @@
+"use strict";
+
+import Common from "../common.js";
+import Log from "./log.js";
+
+
+document.addEventListener("DOMContentLoaded", function() {
+
+  var sobjs = {
+    "logEl": document.getElementById("admin-log-content"),
+    "seaEl": document.getElementById("admin-log-search"),
+    "rspEl": document.getElementById("admin-log-response"),
+  };
+
+  Common.init(sobjs);
+  registerEventHandlers(sobjs);
+  Log.init(sobjs);
+
+});
+
+function registerEventHandlers(sobjs) {
+  sobjs.seaEl.onkeyup = function(e) {
+    if(e.keyCode === 13) {
+      Log.doFetch(sobjs);
+    }
+  }
+}

+ 28 - 0
elfcom-frontend/common.js

@@ -10,11 +10,15 @@ var Common = {
   success: success,
   handleReject: handleReject,
   sendJson: sendJson,
+  minuteString: minuteString,
+  hourString: hourString,
+  dayString: dayString,
   monthString: monthString,
   yearString: yearString,
   selectByIndex: selectByIndex,
   selectById: selectById,
   fileToDataUrl64: fileToDataUrl64,
+  filterTokens: filterTokens,
 };
 export default Common;
 
@@ -82,6 +86,21 @@ function sendJson(url, data) {
   return fetch(url, fInit);
 }
 
+function minuteString(date) {
+  if(date.getMinutes() < 9) { return "0" + date.getMinutes(); }
+  return "" + date.getMinutes();
+}
+
+function hourString(date) {
+  if(date.getHours() < 9) { return "0" + date.getHours(); }
+  return "" + date.getHours();
+}
+
+function dayString(date) {
+  if(date.getDate() < 9) { return "0" + date.getDate(); }
+  return "" + date.getDate();
+}
+
 function monthString(date) {
   if(date.getMonth() < 9) { return "0" + (date.getMonth() + 1); }
   return "" + (date.getMonth() + 1);
@@ -112,3 +131,12 @@ function fileToDataUrl64(file) {
     reader.readAsDataURL(file);
   });
 }
+
+function filterTokens(tokens, key) {
+  for(var tok of tokens) {
+    if(tok.startsWith(key)) {
+      return tok.slice(key.length).replace(/^=/, "");
+    }
+  }
+  return null;
+}

+ 6 - 15
elfcom-frontend/edit/fetch.js

@@ -25,18 +25,18 @@ function init(sobjs) {
 
 function doFetch(sobjs) {
   var tokens = sobjs.seaEl.value.split(";");
-  var soToken = filterTokens(tokens, "so");
-  var poToken = filterTokens(tokens, "po");
-  var doToken = filterTokens(tokens, "do");
-  var dateToken = filterTokens(tokens, "date");
+  var soToken = Common.filterTokens(tokens, "so");
+  var poToken = Common.filterTokens(tokens, "po");
+  var doToken = Common.filterTokens(tokens, "do");
+  var dateToken = Common.filterTokens(tokens, "date");
   var dates = null;
   var cats = null;
   if(dateToken !== null) {
     dates = [ new Date("20" + dateToken.slice(4, 6), dateToken.slice(1, 3)),
               new Date("20" + dateToken.slice(10, 12), dateToken.slice(7, 9))];
   }
-  var titleToken = filterTokens(tokens, "title");
-  var catToken = filterTokens(tokens, "cats");
+  var titleToken = Common.filterTokens(tokens, "title");
+  var catToken = Common.filterTokens(tokens, "cats");
   if(catToken !== null) {
     cats = catToken.split(",").map(a => a.trim()).filter((c) => {
       return c.length > 0;
@@ -83,12 +83,3 @@ function updateSelect(sobjs, items) {
   nopt.id = "-1"; nopt.innerHTML = "New Post";
   sobjs.selEl.add(nopt, 0);
 }
-
-function filterTokens(tokens, key) {
-  for(var tok of tokens) {
-    if(tok.startsWith(key)) {
-      return tok.slice(key.length).replace(/^=/, "");
-    }
-  }
-  return null;
-}

+ 0 - 1
elfcom-frontend/edit/site_edit.js

@@ -31,7 +31,6 @@ document.addEventListener("DOMContentLoaded", function() {
   };
 
   registerEventHandlers(sobjs);
-
   Common.init(sobjs);
   Fetch.init(sobjs)
     .then(() => Load.init(sobjs))