58 Commits

Author SHA1 Message Date
a9959b81fa doc/README: changelog for v3.1.1 2025-08-20 16:24:27 +12:00
247dbd056a archiveserver: add -git suffix to default server header banner 2025-08-20 16:22:02 +12:00
b8e5f8effe go.mod: declare golang language version 2025-08-20 16:21:50 +12:00
48dca86185 makefile: remove obsolete makefile 2025-08-20 16:21:42 +12:00
219ce85b7f static: replace go-bindata usage with standard go:embed 2025-08-20 16:21:31 +12:00
a1ce0c8841 deps: remove vendoring 2025-08-20 16:21:16 +12:00
5e5d4cb253 doc: remove code.ivysaur.me meta tags in README 2018-12-31 18:48:45 +13:00
3a80de8b9e doc: remove TODO.txt (moved to Gitea issues) 2018-11-07 18:38:38 +13:00
7c2c55cb3c doc: move README to root 2018-11-07 18:35:21 +13:00
3f56aac8c1 convert to go modules 2018-11-06 19:21:20 +13:00
8e7bcbe057 hg2git: convert ignores file 2018-11-06 18:58:15 +13:00
0197b8466f hg-git: remove old .hgtags file 2018-11-06 18:57:10 +13:00
944ae74ba8 doc: add TODO page 2017-12-10 15:34:55 +13:00
d10808c7c1 bump makefile version to 3.1.1 2017-12-10 15:22:00 +13:00
e3ff8095ee Added tag release-3.1.0 for changeset 1f276b596c58 2017-12-10 15:21:41 +13:00
901cb84a1c 3.1.0 changelog 2017-12-10 15:20:27 +13:00
2ae9d52419 bump all versions to 3.1.0 2017-12-10 15:20:22 +13:00
f5d587819f makefile: fix devel target for when build fails 2017-12-10 15:19:33 +13:00
dea0cffc90 statistics page 2017-12-10 15:19:20 +13:00
917b9c1a53 rebuild staticResources.go 2017-12-10 14:21:02 +13:00
758ba9a457 makefile: add "devel" target 2017-12-10 14:19:42 +13:00
d1007bb645 skip redirect when searching (if javascript is available) 2017-12-10 14:19:35 +13:00
35b9973a4a mobile-friendly search results 2017-12-10 14:05:45 +13:00
437e1f60e5 preserve line-selection hash fragment in URL when navigating to a search result 2017-12-10 13:57:32 +13:00
3c8631c0d7 fix search highlighting for previous 2017-12-10 13:57:16 +13:00
3f4d5e2522 preserve consecutive whitespaces in chat messages 2017-12-10 13:53:20 +13:00
a087ad2e7b 'latest' link takes you to the latest for the currently selected hub 2017-12-10 13:49:46 +13:00
a27645772a css: instant on, animate off 2017-12-10 13:44:33 +13:00
86cc1cbb2e add nonce for css 2017-12-10 13:44:26 +13:00
9ebe6ca2fb rebuild staticResources.go 2017-12-10 13:37:38 +13:00
117a5fa51e fix disappearing text in dropdown menu 2017-12-10 13:37:33 +13:00
5f2a1b528d fix spaces in searches turning into plusses 2017-12-10 13:37:22 +13:00
559283566b fix #newpage issues 2017-12-10 13:32:35 +13:00
add69c5c81 pretty up the generated HTML (2) 2017-12-10 13:27:40 +13:00
f261ca0ece pretty up the generated HTML 2017-12-10 13:25:34 +13:00
672f0f606c rebuild staticResources.go 2017-12-10 13:04:00 +13:00
cd1e868daf fix phantom space between dom elements; fix search box being split into two lines in some cases; add more padding in mobile layout 2017-12-10 13:03:47 +13:00
e078383e6d add archiveState.URL() method 2017-12-10 12:50:19 +13:00
8568c4bdc2 replace js state setting, with URL targets in select values ; remove "PCRE" from regex tooltip 2017-12-10 12:50:11 +13:00
7b9aece4d2 use pathEscape / pathUnescape for query/rx URLs (fixes + in regex) 2017-12-10 12:48:24 +13:00
e0ff64cd22 move menucontainer into a single div, prevent text selection on dropdown elements 2017-12-10 12:20:18 +13:00
3f623782b7 use a constant for default font size 2017-12-10 12:19:48 +13:00
92e7064372 inline the toggle_element function 2017-12-10 12:19:35 +13:00
149e226729 move toggleMenu call from html onclick to DOM addEventListener 2017-12-10 12:19:07 +13:00
3c3028b8d1 css: remove unused style block 2017-12-10 12:18:37 +13:00
8a452c0fa6 standardise html-escaping functions 2017-12-10 12:08:22 +13:00
693f541934 rebuild staticResources.go 2017-12-10 12:03:14 +13:00
e6ed43154c replace star png image with inline SVG 2017-12-10 12:03:08 +13:00
533527c890 remove mousetrap.js (APL) in favour of accesskey; add title tags 2017-12-10 11:57:59 +13:00
f28ae17a00 fix legacy index.php still being routable(!) 2017-12-10 11:43:02 +13:00
cd879798e3 bump all versions to 3.0.2 2017-09-06 18:19:57 +12:00
f90a6d1dc6 Added tag release-3.0.1 for changeset 47147713ae1b 2017-09-06 18:19:47 +12:00
ba097e3a9d rebuild staticResources.go 2017-09-06 18:18:25 +12:00
0dd72a0468 bump all versions to 3.0.1 2017-09-06 18:18:17 +12:00
d91513c1ac doc: changelog 2017-09-06 18:17:31 +12:00
957195ca11 convert time specs to strftime 2017-09-06 18:17:25 +12:00
bed15bfb33 track github.com/jehiah/go-strftime (ISC, git-2efbe75) 2017-09-06 18:17:14 +12:00
7641f9d73d Added tag release-3.0.0 for changeset 718fcb17fc0c 2017-08-13 17:14:49 +12:00
17 changed files with 413 additions and 560 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
# development
cmd/archive-server/archive-server
cmd/archive-server/config.json
# binaries
_dist/
build/

View File

@@ -1,9 +0,0 @@
mode:regex
# development
^cmd/archive-server/archive-server$
^cmd/archive-server/config.json$
# binaries
^_dist/
^build/

View File

@@ -5,10 +5,12 @@ import (
"fmt" "fmt"
"regexp" "regexp"
"time" "time"
"github.com/jehiah/go-strftime"
) )
var ( var (
SERVER_VERSION = "archive/0.0.0" SERVER_VERSION = "archive/0.0.0-git"
) )
type ArchiveServer struct { type ArchiveServer struct {
@@ -16,7 +18,7 @@ type ArchiveServer struct {
cfg *Config cfg *Config
startup time.Time startup time.Time
rxViewRoot, rxViewPage, rxSearch, rxSearchRx *regexp.Regexp rxViewRoot, rxViewPage, rxSearch, rxSearchRx, rxStats *regexp.Regexp
} }
func NewArchiveServer(cfg *Config) (*ArchiveServer, error) { func NewArchiveServer(cfg *Config) (*ArchiveServer, error) {
@@ -47,6 +49,7 @@ func NewArchiveServer(cfg *Config) (*ArchiveServer, error) {
rxViewPage: regexp.MustCompile(`^/([^/]+)/(\d+)/(\d+)/(?:page-)?(\d+)$`), rxViewPage: regexp.MustCompile(`^/([^/]+)/(\d+)/(\d+)/(?:page-)?(\d+)$`),
rxSearch: regexp.MustCompile(`^/([^/]+)/search/(.*)$`), rxSearch: regexp.MustCompile(`^/([^/]+)/search/(.*)$`),
rxSearchRx: regexp.MustCompile(`^/([^/]+)/rx/(.*)$`), rxSearchRx: regexp.MustCompile(`^/([^/]+)/rx/(.*)$`),
rxStats: regexp.MustCompile((`^/([^/]+)/stats/?$`)),
}, nil }, nil
} }
@@ -57,7 +60,7 @@ func (this *ArchiveServer) LogFile(ls *LogSource, ym YearMonth) (string, error)
for _, fl := range ls.FileLocation { for _, fl := range ls.FileLocation {
if fl.StartMonth.Index() <= ymIndex && (fl.EndMonth == nil || fl.EndMonth.Index() >= ymIndex) { if fl.StartMonth.Index() <= ymIndex && (fl.EndMonth == nil || fl.EndMonth.Index() >= ymIndex) {
return time.Date(ym.Year, ym.Month, 1, 0, 0, 0, 0, this.timezone).Format(fl.LogFilePath), nil return strftime.Format(fl.LogFilePath, time.Date(ym.Year, ym.Month, 1, 0, 0, 0, 0, this.timezone)), nil
} }
} }

View File

@@ -2,14 +2,18 @@ package archive
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"html/template" "html"
"io/ioutil" "io/ioutil"
"math" "math"
"net/http" "net/http"
"net/url"
"os" "os"
"regexp" "regexp"
"sort"
"strings" "strings"
"time"
) )
const ( const (
@@ -22,6 +26,7 @@ type ArchiveState struct {
logBestSlug string logBestSlug string
query string query string
queryIsRegex bool queryIsRegex bool
isStats bool
ym YearMonth ym YearMonth
page int page int
highestPage int highestPage int
@@ -34,6 +39,30 @@ func NewArchiveState(svr *ArchiveServer) *ArchiveState {
} }
} }
func (this *ArchiveState) showPageURLs() bool {
return this.log != nil && len(this.query) == 0 && !this.isStats
}
func (this *ArchiveState) URL() string {
if this.isStats {
return fmt.Sprintf(`/%s/stats`, this.logBestSlug)
} else if len(this.query) > 0 {
if this.queryIsRegex {
return fmt.Sprintf(`/%s/rx/%s`, this.logBestSlug, url.QueryEscape(this.query))
} else {
return fmt.Sprintf(`/%s/search/%s`, this.logBestSlug, url.QueryEscape(this.query))
}
} else {
if this.page == pageNotSet {
return fmt.Sprintf(`/%s/%s/%s`, this.logBestSlug, this.ym.Year, this.ym.Month)
} else {
return fmt.Sprintf(`/%s/%s/%s/page-%d`, this.logBestSlug, this.ym.Year, this.ym.Month, this.page)
}
}
}
func (this *ArchiveState) selectSource(log *LogSource, slug string) { func (this *ArchiveState) selectSource(log *LogSource, slug string) {
this.log = log this.log = log
this.logBestSlug = slug this.logBestSlug = slug
@@ -57,6 +86,11 @@ func (this *ArchiveState) renderView(w http.ResponseWriter) {
lines := strings.Split(string(fc), "\n") lines := strings.Split(string(fc), "\n")
// Work around #newpage: if the last line is blank, skip
if len(lines) > 0 && len(lines[len(lines)-1]) == 0 {
lines = lines[:len(lines)-1]
}
this.highestPage = int(math.Ceil(float64(len(lines))/float64(this.svr.cfg.LinesPerPage))) - 1 this.highestPage = int(math.Ceil(float64(len(lines))/float64(this.svr.cfg.LinesPerPage))) - 1
if this.page == pageNotSet || this.page > this.highestPage { if this.page == pageNotSet || this.page > this.highestPage {
this.page = this.highestPage this.page = this.highestPage
@@ -73,12 +107,136 @@ func (this *ArchiveState) renderView(w http.ResponseWriter) {
output := "" output := ""
for i := startLine; i < endLine; i += 1 { for i := startLine; i < endLine; i += 1 {
output += template.HTMLEscapeString(lines[i]) + "<br>\n" output += html.EscapeString(lines[i]) + "\n"
} }
this.renderTemplate(w, []byte(output)) this.renderTemplate(w, []byte(output))
} }
// parseStatsFor loads user post statistics for a single yearMonth in a single log source.
func (this *ArchiveState) parseStatsFor(ls *LogSource, ym YearMonth, into map[string]int) error {
fname, err := this.svr.LogFile(ls, ym)
if err != nil {
return err
}
fc, err := ioutil.ReadFile(fname)
if err != nil {
return err
}
rxUser := regexp.MustCompile(`(?ms)^[^<\r\n]*<([^>\r\n]+)>.+?$`)
matches := rxUser.FindAllSubmatch(fc, -1)
if matches == nil || len(matches) == 0 {
return errors.New("No matches")
}
for _, match := range matches {
username := string(match[1])
if ct, ok := into[username]; ok {
into[username] = ct + 1
} else {
into[username] = 1
}
}
return nil
}
func (this *ArchiveState) renderStats(w http.ResponseWriter) {
// Lines per year
// Users / posts/year
startTime := time.Now()
yearsToUsersToPostCount := make(map[int]map[string]int, 0)
totalErrors := 0
var lastError error = nil
ym := this.log.EarliestDate()
orderedYears := make([]int, 0)
for {
usersToPostCount, ok := yearsToUsersToPostCount[ym.Year]
if !ok {
usersToPostCount = make(map[string]int)
orderedYears = append(orderedYears, ym.Year)
}
err := this.parseStatsFor(this.log, ym, usersToPostCount)
if err != nil {
//log.Printf("Stats(%s): %s", this.logBestSlug, err.Error())
totalErrors += 1
lastError = err
}
//fmt.Printf("%#v\n", usersToPostCount)
yearsToUsersToPostCount[ym.Year] = usersToPostCount
if ym.Equals(this.log.LatestDate()) {
break
}
ym = ym.Next()
}
ret := make([]byte, 0)
if lastError != nil {
ret = append(ret, []byte(fmt.Sprintf("<em>Got %d errors, including: '%s'</em>\n\n", totalErrors, lastError.Error()))...)
}
//
allUsersExistence := make(map[string]struct{})
for _, usersMap := range yearsToUsersToPostCount {
for username, _ := range usersMap {
allUsersExistence[username] = struct{}{}
}
}
allUsernames := make([]string, 0, len(allUsersExistence))
for username, _ := range allUsersExistence {
allUsernames = append(allUsernames, username)
}
sort.Strings(allUsernames)
//
ret = append(ret, []byte(`<table><thead><tr><th>&nbsp;</th>`)...)
for _, year := range orderedYears {
ret = append(ret, []byte(fmt.Sprintf(`<th>%d</th>`, year))...)
}
ret = append(ret, []byte("</tr><tbody>\n")...)
for _, username := range allUsernames {
ret = append(ret, []byte(fmt.Sprintf(`<tr><td>%s</td>`, html.EscapeString(username)))...)
for _, year := range orderedYears {
usersMap := yearsToUsersToPostCount[year]
posts, _ /*ok*/ := usersMap[username]
ret = append(ret, []byte(fmt.Sprintf(`<td>%d</td>`, posts))...)
}
ret = append(ret, []byte("</tr>\n")...)
}
ret = append(ret, []byte(`</tbody><tfoot><th style="text-align:right;">TOTAL:</th>`)...)
for _, year := range orderedYears {
postsTotalForYear := 0
for _, userPostCount := range yearsToUsersToPostCount[year] {
postsTotalForYear += userPostCount
}
ret = append(ret, []byte(fmt.Sprintf(`<th>%d</th>`, postsTotalForYear))...)
}
duration := time.Now().Sub(startTime)
ret = append(ret, []byte(fmt.Sprintf("</tfoot></table>\n\n<em>%d total users\nStatistics generated in %s</em>", len(allUsernames), duration.String()))...)
this.renderTemplate(w, ret)
}
// renderSearch renders the search results. // renderSearch renders the search results.
// - Mandatory: log, query, queryIsRegex // - Mandatory: log, query, queryIsRegex
func (this *ArchiveState) renderSearch(w http.ResponseWriter) { func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
@@ -101,7 +259,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
this.renderTemplateHead(w) this.renderTemplateHead(w)
totalResults := 0 totalResults := 0
w.Write([]byte(`<ul>`)) w.Write([]byte(`<ul class="search-results">`))
limit := this.log.LatestDate().Next() // one off the end limit := this.log.LatestDate().Next() // one off the end
for ympair := this.log.EarliestDate(); !ympair.Equals(limit); ympair = ympair.Next() { for ympair := this.log.EarliestDate(); !ympair.Equals(limit); ympair = ympair.Next() {
@@ -131,7 +289,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
lineNo := i % this.svr.cfg.LinesPerPage lineNo := i % this.svr.cfg.LinesPerPage
url := fmt.Sprintf(`/%s/%d/%d/page-%d#line-%d`, this.logBestSlug, ympair.Year, ympair.Month, page, lineNo) url := fmt.Sprintf(`/%s/%d/%d/page-%d#line-%d`, this.logBestSlug, ympair.Year, ympair.Month, page, lineNo)
w.Write([]byte(`<li><a href="` + template.HTMLEscapeString(url) + `">&raquo;</a> ` + template.HTMLEscapeString(scanner.Text()) + `</li>`)) w.Write([]byte(`<li><a href="` + html.EscapeString(url) + `">&raquo;</a> ` + html.EscapeString(scanner.Text()) + `</li>`))
} }
}() }()
@@ -140,7 +298,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
w.Write([]byte(`</ul>`)) w.Write([]byte(`</ul>`))
if totalResults == 0 { if totalResults == 0 {
w.Write([]byte(`No search results for &quot;<em>` + template.HTMLEscapeString(this.query) + `</em>&quot;`)) w.Write([]byte(`No search results for &quot;<em>` + html.EscapeString(this.query) + `</em>&quot;`))
} else { } else {
w.Write([]byte(`<br><em>Found ` + fmt.Sprintf("%d", totalResults) + ` total result(s).</em><br><br>`)) w.Write([]byte(`<br><em>Found ` + fmt.Sprintf("%d", totalResults) + ` total result(s).</em><br><br>`))
} }
@@ -150,7 +308,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
// renderError renders a plain text string, escaping it for HTML use. // renderError renders a plain text string, escaping it for HTML use.
func (this *ArchiveState) renderError(w http.ResponseWriter, msg string) { func (this *ArchiveState) renderError(w http.ResponseWriter, msg string) {
this.renderTemplate(w, []byte(template.HTMLEscapeString(msg))) this.renderTemplate(w, []byte(html.EscapeString(msg)))
} }
func (this *ArchiveState) renderTemplate(w http.ResponseWriter, body []byte) { func (this *ArchiveState) renderTemplate(w http.ResponseWriter, body []byte) {
@@ -169,55 +327,70 @@ func (this *ArchiveState) renderTemplateHead(w http.ResponseWriter) {
title = this.log.Description + ` Archives` title = this.log.Description + ` Archives`
} }
showPageURLs := (this.log != nil && len(this.query) == 0) latestUrl := `/`
if this.log != nil {
latestUrl = fmt.Sprintf(`/%s/%d/%d`, url.PathEscape(this.logBestSlug), this.log.LatestDate().Year, this.log.LatestDate().Month)
}
statsUrl := fmt.Sprintf(`/%s/stats`, url.PathEscape(this.logBestSlug))
w.Write([]byte(`<!DOCTYPE html> w.Write([]byte(`<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>` + template.HTMLEscapeString(title) + `</title> <title>` + html.EscapeString(title) + `</title>
<link rel="stylesheet" type="text/css" href="/style.css"> <link rel="stylesheet" type="text/css" href="/style.css?nonce=` + fmt.Sprintf("%d", this.svr.startup.Unix()) + `">
</head> </head>
<body> <body>
<div class="layout-top nav"> <div class="layout-top nav">
<div id="tr1" style="display:none;"></div> <div id="menu-container" class="noselect">
<div id="tr2" style="display:none;"></div> <div id="tr1"></div>
<div class="ddmenu" id="spm" style="display:none;"> <div id="tr2"></div>
<a href="/">Latest</a> <div class="ddmenu">
<a href="` + html.EscapeString(latestUrl) + `">Latest</a>
<a href="` + html.EscapeString(statsUrl) + `">Statistics</a>
<a onclick="fontSize(1);">Font increase</a> <a onclick="fontSize(1);">Font increase</a>
<a onclick="fontSize(-1);">Font decrease</a> <a onclick="fontSize(-1);">Font decrease</a>
<a href="/download" onclick="return confirm('Are you sure you want to download a backup?');">Download backup</a> <a href="/download" onclick="return confirm('Are you sure you want to download a backup?');">Download backup</a>
</div> </div>
</div>
<a onclick="toggleMenu();"><div id="logo" class="layout-pushdown"></div></a> <div id="logo" class="layout-pushdown"><svg viewBox="0 0 24 24">
<path fill="#000000" d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z" />
</svg></div>
<span class="area-nav"> <span class="area-nav">
<form method="GET" id="frmHub"> <select onchange="window.location.pathname = this.value;">
<select name="h" id="selHub"> `))
`))
for i, h := range this.svr.cfg.Logs { for i, h := range this.svr.cfg.Logs {
slug, _ := this.svr.bestSlugFor(&this.svr.cfg.Logs[i]) slug, _ := this.svr.bestSlugFor(&this.svr.cfg.Logs[i])
current := (this.log == &this.svr.cfg.Logs[i]) current := (this.log == &this.svr.cfg.Logs[i])
w.Write([]byte(`<option value="` + template.HTMLEscapeString(slug) + `" ` + attr(current, "selected") + `>` + template.HTMLEscapeString(h.Description) + `</option>`)) targetUri := `/` + url.PathEscape(slug) + `/`
if len(this.query) > 0 {
if this.queryIsRegex {
targetUri += `rx/` + url.PathEscape(this.query)
} else {
targetUri += `search/` + url.PathEscape(this.query)
}
} else {
targetUri += fmt.Sprintf(`%d/%d`, h.LatestDate().Year, h.LatestDate().Month)
}
w.Write([]byte(fmt.Sprintf("\t\t\t\t\t<option value=\"%s\"%s>%s</option>\n", html.EscapeString(targetUri), attr(current, " selected"), html.EscapeString(h.Description))))
} }
w.Write([]byte(` w.Write([]byte(`
</select> </select>
</form>
`)) `))
if showPageURLs { if this.showPageURLs() {
w.Write([]byte(` w.Write([]byte(`
<select onchange="window.location.pathname = this.value;">
<form method="GET"> `))
<input type="hidden" name="h" value="` + template.HTMLEscapeString(this.logBestSlug) + `">
<select id="seldate" onchange="setYM(this);">
`))
// Generate month dropdown options // Generate month dropdown options
@@ -226,14 +399,24 @@ func (this *ArchiveState) renderTemplateHead(w http.ResponseWriter) {
for ympair := this.log.EarliestDate(); !ympair.Equals(limit); ympair = ympair.Next() { for ympair := this.log.EarliestDate(); !ympair.Equals(limit); ympair = ympair.Next() {
if ympair.Year != lastY { if ympair.Year != lastY {
if lastY != -1 { if lastY != -1 {
w.Write([]byte(`</optgroup>`)) w.Write([]byte("\t\t\t\t\t</optgroup>\n"))
} }
w.Write([]byte(`<optgroup label="` + fmt.Sprintf("%d", ympair.Year) + `">`)) w.Write([]byte(fmt.Sprintf("\t\t\t\t\t<optgroup label=\"%d\">\n", ympair.Year)))
lastY = ympair.Year lastY = ympair.Year
} }
w.Write([]byte(fmt.Sprintf(`<option value="%d-%d" %s>%s</option>`, ympair.Year, ympair.Month, attr(ympair.Equals(this.ym), "selected"), template.HTMLEscapeString(ympair.Month.String())))) targetUri := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, ympair.Year, ympair.Month)
w.Write([]byte(fmt.Sprintf("\t\t\t\t\t\t<option value=\"%s\"%s>%s</option>\n", html.EscapeString(targetUri), attr(ympair.Equals(this.ym), " selected"), html.EscapeString(ympair.Month.String()))))
} }
if lastY != -1 {
w.Write([]byte("\t\t\t\t\t</optgroup>\n"))
}
w.Write([]byte(`
</select>
<div class="mini-separator layout-pushdown"></div>
`))
// //
@@ -249,51 +432,46 @@ func (this *ArchiveState) renderTemplateHead(w http.ResponseWriter) {
nextPage = this.highestPage nextPage = this.highestPage
} }
prevMonth := this.ym.Prev() // FIXME check bounds
prevMonthLink := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, prevMonth.Year, prevMonth.Month)
nextMonth := this.ym.Next() // FIXME check bounds
nextMonthLink := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, nextMonth.Year, nextMonth.Month)
w.Write([]byte(` w.Write([]byte(`
</optgroup> <a style="display:none;" id="monthprev" accesskey="j" href="` + html.EscapeString(prevMonthLink) + `">
<a style="display:none;" id="monthnext" accesskey="k" href="` + html.EscapeString(nextMonthLink) + `">
</select> <a class="btn" href="` + pageBase + `/page-0">&laquo;</a>
<input type="hidden" name="y" id="f_y" value=""> <a class="btn" accesskey="h" title="Previous page (Alt+H)" href="` + pageBase + `/page-` + fmt.Sprintf("%d", previousPage) + `">&lsaquo;</a>
<input type="hidden" name="m" id="f_m" value=""> <span class="current-page">` + fmt.Sprintf("%d", this.page) + `</span>
<input type="hidden" name="p" value="0" > <a class="btn" accesskey="l" title="Next page (Alt+L)" href="` + pageBase + `/page-` + fmt.Sprintf("%d", nextPage) + `">&rsaquo;</a>
</form> <a class="btn" href="` + pageBase + `">&raquo;</a>
<div class="mini-separator layout-pushdown"></div>
<a class="btn" href="` + pageBase + `/page-0">&laquo;</a><a
class="btn" id="pgprev" href="` + pageBase + `/page-` + fmt.Sprintf("%d", previousPage) + `">&lsaquo;</a>
` + fmt.Sprintf("%d", this.page) + `
<a class="btn" id="pgnext" href="` + pageBase + `/page-` + fmt.Sprintf("%d", nextPage) + `">&rsaquo;</a><a
class="btn" href="` + pageBase + `">&raquo;</a>
`)) `))
} }
w.Write([]byte(` w.Write([]byte(`
<div class="pad"></div>
</span> </span>
<span class="area-search"> <span class="area-search">
<form method="GET" id="search-form">
<form method="GET"> <input type="hidden" name="h" value="` + html.EscapeString(this.logBestSlug) + `">
<input type="hidden" name="h" value="` + template.HTMLEscapeString(this.logBestSlug) + `"> <input type="text" id="searchbox" name="q" value="` + html.EscapeString(this.query) + `" placeholder="Search..." accesskey="m" title="Search (Alt+M)">
<input type="text" id="searchbox" name="q" value="` + template.HTMLEscapeString(this.query) + `" placeholder="Search...">
<input type="submit" value="&raquo;"> <input type="submit" value="&raquo;">
<input type="checkbox" class="layout-pushdown" name="rx" value="1" title="PCRE Regular Expression" ` + attr(this.queryIsRegex, "checked") + `> <input type="checkbox" class="layout-pushdown" name="rx" value="1" title="Regular Expression" ` + attr(this.queryIsRegex, "checked") + `>
</form> </form>
</span> </span>
</div> </div>
<div class="layout-body" id="chatarea"> <div class="layout-body" id="chatarea">`,
`)) ))
// Header ends // Header ends
} }
func (this *ArchiveState) renderTemplateFoot(w http.ResponseWriter) { func (this *ArchiveState) renderTemplateFoot(w http.ResponseWriter) {
w.Write([]byte(` w.Write([]byte(`</div>
</div>
<script type="text/javascript" src="/archive.js?nonce=` + fmt.Sprintf("%d", this.svr.startup.Unix()) + `"></script> <script type="text/javascript" src="/archive.js?nonce=` + fmt.Sprintf("%d", this.svr.startup.Unix()) + `"></script>
</body> </body>
</html> </html>

View File

@@ -1,79 +0,0 @@
#
# Makefile for archive
#
VERSION:=3.0.0
SOURCES:=Makefile \
static \
cmd $(wildcard cmd/archive-server/*.go) \
$(wildcard *.go)
GOFLAGS := -ldflags='-s -w -X code.ivysaur.me/archive.SERVER_VERSION=archive/${VERSION}' -gcflags='-trimpath=$(GOPATH)' -asmflags='-trimpath=$(GOPATH)'
#
# Phony targets
#
.PHONY: all dist clean
all: build/linux64/archive-server build/win32/archive-server.exe
dist: \
_dist/archive-$(VERSION)-linux64.tar.gz \
_dist/archive-$(VERSION)-win32.7z \
_dist/archive-$(VERSION)-src.zip
clean:
if [ -f ./staticResources.go ] ; then rm ./staticResources.go ; fi
if [ -d ./build ] ; then rm -r ./build ; fi
if [ -f ./archive ] ; then rm ./archive ; fi
#
# Generated files
#
staticResources.go: static/ static/*
go-bindata -o staticResources.go -prefix static -pkg archive static
#
# Release artefacts
#
build/linux64/archive-server: $(SOURCES) staticResources.go
mkdir -p build/linux64
(cd cmd/archive-server ; \
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
go build $(GOFLAGS) -o ../../build/linux64/archive-server \
)
build/win32/archive-server.exe: $(SOURCES) staticResources.go
mkdir -p build/win32
(cd cmd/archive-server ; \
PATH=/usr/lib/mxe/usr/bin:$(PATH) CC=i686-w64-mingw32.static-gcc \
CGO_ENABLED=1 GOOS=windows GOARCH=386 \
go build $(GOFLAGS) -o ../../build/win32/archive-server.exe \
)
build/linux64/config.json.SAMPLE: cmd/archive-server/config.json.SAMPLE
cp cmd/archive-server/config.json.SAMPLE build/linux64/config.json.SAMPLE
build/win32/config.json.SAMPLE: cmd/archive-server/config.json.SAMPLE
cp cmd/archive-server/config.json.SAMPLE build/win32/config.json.SAMPLE
_dist/archive-$(VERSION)-linux64.tar.gz: build/linux64/archive-server build/linux64/config.json.SAMPLE
mkdir -p _dist
tar caf _dist/archive-$(VERSION)-linux64.tar.gz -C build/linux64 archive-server config.json.SAMPLE --owner=0 --group=0
_dist/archive-$(VERSION)-win32.7z: build/win32/archive-server.exe build/win32/config.json.SAMPLE
mkdir -p _dist
( cd build/win32 ; \
if [ -f dist.7z ] ; then rm dist.7z ; fi ; \
7z a dist.7z archive-server.exe config.json.SAMPLE ; \
mv dist.7z ../../_dist/archive-$(VERSION)-win32.7z \
)
_dist/archive-$(VERSION)-src.zip: $(SOURCES)
hg archive --type=zip _dist/archive-$(VERSION)-src.zip

View File

@@ -1,10 +1,39 @@
# archive
![](https://img.shields.io/badge/written%20in-Go-blue.svg)
A web interface for browsing chat logs. A web interface for browsing chat logs.
As of the 3.0 release, `archive` is available as a standalone binary for Linux and Windows. As of the 3.0 release, `archive` is available as a standalone binary for Linux and Windows.
Written in Golang, PHP ## Changelog
=CHANGELOG= 2025-08-20: 3.1.1
- Update dependencies, replace go-bindata with `go:embed`
2018-11-06: (no release tag)
- Convert from hg to git
- Convert to Go Modules
2017-12-10: 3.1.0
- Feature: Statistics page
- Enhancement: Upgrade corner menu image to support high DPI
- Enhancement: Mobile-friendly buttons in search results
- Enhancement: Explain keyboard shortcuts when hovering over form elements
- Enhancement: Improve performance when changing sources and when searching, by removing a network roundtrip
- Enhancement: "Latest" link takes you to the latest for the current data source
- Change ctrl+alt+[H/J/K/L/M]... keyboard shortcuts to just alt+[...], remove dependency on APL `mousetrap.js`
- Fix an issue with accessing the legacy php controller after the Go port
- Fix an issue with using plus characters in regular expression searches
- Fix an issue with blank pages appearing if there was a divisible number of log entries
- Fix an issue with browsers using stale stylesheets
- Fix a cosmetic issue with text selection when using dropdown menu items
- Fix a cosmetic issue with PCRE text in regular expression hover message (it's no longer PCRE)
- Fix a cosmetic issue with search box layout at some screen sizes
- Fix a cosmetic issue with animation responsiveness
2017-09-06: 3.0.1
- Breaking: Revert date formatting in filenames back to strftime-compatible
2017-08-13: 3.0.0 2017-08-13: 3.0.0
- Rewritten in Go. - Rewritten in Go.

View File

@@ -2,6 +2,7 @@ package archive
import ( import (
"bytes" "bytes"
"embed"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@@ -10,6 +11,11 @@ import (
"time" "time"
) )
// Static assets
//go:embed static/*
var staticAssets embed.FS
func (this *ArchiveServer) lookupSourceByNumericString(slug string) *LogSource { func (this *ArchiveServer) lookupSourceByNumericString(slug string) *LogSource {
intval, err := strconv.Atoi(slug) intval, err := strconv.Atoi(slug)
if err != nil { if err != nil {
@@ -71,7 +77,7 @@ func (this *ArchiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Handle static assets // Handle static assets
static, err := Asset(r.URL.Path[1:]) static, err := staticAssets.ReadFile(`static/` + r.URL.Path[1:])
if err == nil { if err == nil {
http.ServeContent(w, r, r.URL.Path[1:], this.startup, bytes.NewReader(static)) http.ServeContent(w, r, r.URL.Path[1:], this.startup, bytes.NewReader(static))
return return
@@ -121,7 +127,7 @@ func (this *ArchiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} else if matches := this.rxSearch.FindStringSubmatch(r.URL.Path); len(matches) > 0 { } else if matches := this.rxSearch.FindStringSubmatch(r.URL.Path); len(matches) > 0 {
if ls := this.lookupSource(matches[1]); ls != nil { if ls := this.lookupSource(matches[1]); ls != nil {
arc.selectSource(ls, matches[1]) arc.selectSource(ls, matches[1])
arc.query, _ = url.QueryUnescape(matches[2]) arc.query, _ = url.PathUnescape(matches[2])
arc.queryIsRegex = false arc.queryIsRegex = false
arc.renderSearch(w) arc.renderSearch(w)
@@ -132,13 +138,24 @@ func (this *ArchiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} else if matches := this.rxSearchRx.FindStringSubmatch(r.URL.Path); len(matches) > 0 { } else if matches := this.rxSearchRx.FindStringSubmatch(r.URL.Path); len(matches) > 0 {
if ls := this.lookupSource(matches[1]); ls != nil { if ls := this.lookupSource(matches[1]); ls != nil {
arc.selectSource(ls, matches[1]) arc.selectSource(ls, matches[1])
arc.query, _ = url.QueryUnescape(matches[2]) arc.query, _ = url.PathUnescape(matches[2])
arc.queryIsRegex = true arc.queryIsRegex = true
arc.renderSearch(w) arc.renderSearch(w)
} else { } else {
arc.renderError(w, fmt.Sprintf("Unknown source '%s'", matches[1])) arc.renderError(w, fmt.Sprintf("Unknown source '%s'", matches[1]))
} }
} else if matches := this.rxStats.FindStringSubmatch(r.URL.Path); len(matches) > 0 {
if ls := this.lookupSource(matches[1]); ls != nil {
arc.selectSource(ls, matches[1])
arc.isStats = true
arc.renderStats(w)
} else {
arc.renderError(w, fmt.Sprintf("Unknown source '%s'", matches[1]))
}
} else { } else {
arc.renderError(w, "Unknown route.") arc.renderError(w, "Unknown route.")
@@ -157,9 +174,9 @@ func (this *ArchiveServer) legacyRoute(w http.ResponseWriter, r *http.Request) {
if u.hasGet("q") { if u.hasGet("q") {
if u.hasGet("rx") { if u.hasGet("rx") {
u.redirectf(`/%d/rx/%s`, hubid, url.QueryEscape(u.get("q"))) u.redirectf(`/%d/rx/%s`, hubid, url.PathEscape(u.get("q")))
} else { } else {
u.redirectf(`/%d/search/%s`, hubid, url.QueryEscape(u.get("q"))) u.redirectf(`/%d/search/%s`, hubid, url.PathEscape(u.get("q")))
} }
} else if u.hasGet("y") && u.hasGet("m") { } else if u.hasGet("y") && u.hasGet("m") {

View File

@@ -27,6 +27,14 @@ func (ym YearMonth) Next() YearMonth {
} }
} }
func (ym YearMonth) Prev() YearMonth {
if ym.Month == time.January {
return YearMonth{Year: ym.Year - 1, Month: time.December}
} else {
return YearMonth{Year: ym.Year, Month: ym.Month - 1}
}
}
// Index returns a single int that can be used to compare this YearMonth with // Index returns a single int that can be used to compare this YearMonth with
// other YearMonth objects. // other YearMonth objects.
func (ym YearMonth) Index() int { func (ym YearMonth) Index() int {

View File

@@ -25,9 +25,9 @@
/* /*
* The full path to the log file. * The full path to the log file.
* Date formatting reference: https://golang.org/pkg/time/#pkg-constants * Date formatting reference: http://strftime.org/
*/ */
"LogFilePath": "/path/to/my-log-source/2006-01.log", "LogFilePath": "/path/to/my-log-source/%Y-%m.log",
/* /*
* The first month for which this path is valid. * The first month for which this path is valid.

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module code.ivysaur.me/archive
go 1.24.4
require github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869

2
go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 h1:IPJ3dvxmJ4uczJe5YQdrYB16oTJlGSC/OyZDqUk9xX4=
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag=

Binary file not shown.

Before

Width:  |  Height:  |  Size: 537 B

View File

@@ -1,27 +1,9 @@
/* mousetrap v1.4.6 craig.is/killing/mice */
(function(J,r,f){function s(a,b,d){a.addEventListener?a.addEventListener(b,d,!1):a.attachEvent("on"+b,d)}function A(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return h[a.which]?h[a.which]:B[a.which]?B[a.which]:String.fromCharCode(a.which).toLowerCase()}function t(a){a=a||{};var b=!1,d;for(d in n)a[d]?b=!0:n[d]=0;b||(u=!1)}function C(a,b,d,c,e,v){var g,k,f=[],h=d.type;if(!l[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(g=0;g<l[a].length;++g)if(k=
l[a][g],!(!c&&k.seq&&n[k.seq]!=k.level||h!=k.action||("keypress"!=h||d.metaKey||d.ctrlKey)&&b.sort().join(",")!==k.modifiers.sort().join(","))){var m=c&&k.seq==c&&k.level==v;(!c&&k.combo==e||m)&&l[a].splice(g,1);f.push(k)}return f}function K(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function x(a,b,d,c){m.stopCallback(b,b.target||b.srcElement,d,c)||!1!==a(b,d)||(b.preventDefault?b.preventDefault():b.returnValue=!1,b.stopPropagation?
b.stopPropagation():b.cancelBubble=!0)}function y(a){"number"!==typeof a.which&&(a.which=a.keyCode);var b=A(a);b&&("keyup"==a.type&&z===b?z=!1:m.handleKey(b,K(a),a))}function w(a){return"shift"==a||"ctrl"==a||"alt"==a||"meta"==a}function L(a,b,d,c){function e(b){return function(){u=b;++n[a];clearTimeout(D);D=setTimeout(t,1E3)}}function v(b){x(d,b,a);"keyup"!==c&&(z=A(b));setTimeout(t,10)}for(var g=n[a]=0;g<b.length;++g){var f=g+1===b.length?v:e(c||E(b[g+1]).action);F(b[g],f,c,a,g)}}function E(a,b){var d,
c,e,f=[];d="+"===a?["+"]:a.split("+");for(e=0;e<d.length;++e)c=d[e],G[c]&&(c=G[c]),b&&"keypress"!=b&&H[c]&&(c=H[c],f.push("shift")),w(c)&&f.push(c);d=c;e=b;if(!e){if(!p){p={};for(var g in h)95<g&&112>g||h.hasOwnProperty(g)&&(p[h[g]]=g)}e=p[d]?"keydown":"keypress"}"keypress"==e&&f.length&&(e="keydown");return{key:c,modifiers:f,action:e}}function F(a,b,d,c,e){q[a+":"+d]=b;a=a.replace(/\s+/g," ");var f=a.split(" ");1<f.length?L(a,f,b,d):(d=E(a,d),l[d.key]=l[d.key]||[],C(d.key,d.modifiers,{type:d.action},
c,a,e),l[d.key][c?"unshift":"push"]({callback:b,modifiers:d.modifiers,action:d.action,seq:c,level:e,combo:a}))}var h={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",18:"alt",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",224:"meta"},B={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},H={"~":"`","!":"1",
"@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},G={option:"alt",command:"meta","return":"enter",escape:"esc",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p,l={},q={},n={},D,z=!1,I=!1,u=!1;for(f=1;20>f;++f)h[111+f]="f"+f;for(f=0;9>=f;++f)h[f+96]=f;s(r,"keypress",y);s(r,"keydown",y);s(r,"keyup",y);var m={bind:function(a,b,d){a=a instanceof Array?a:[a];for(var c=0;c<a.length;++c)F(a[c],b,d);return this},
unbind:function(a,b){return m.bind(a,function(){},b)},trigger:function(a,b){if(q[a+":"+b])q[a+":"+b]({},a);return this},reset:function(){l={};q={};return this},stopCallback:function(a,b){return-1<(" "+b.className+" ").indexOf(" mousetrap ")?!1:"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable},handleKey:function(a,b,d){var c=C(a,b,d),e;b={};var f=0,g=!1;for(e=0;e<c.length;++e)c[e].seq&&(f=Math.max(f,c[e].level));for(e=0;e<c.length;++e)c[e].seq?c[e].level==f&&(g=!0,
b[c[e].seq]=1,x(c[e].callback,d,c[e].combo,c[e].seq)):g||x(c[e].callback,d,c[e].combo);c="keypress"==d.type&&I;d.type!=u||w(a)||c||t(b);I=g&&"keydown"==d.type}};J.Mousetrap=m;"function"===typeof define&&define.amd&&define(m)})(window,document);
/* archive.js */ /* archive.js */
function i(s) { var alreadyLoaded = false;
return document.getElementById(s);
}
function t(e) { var $chatArea = document.getElementById("chatarea");
e.style.display = (e.style.display == 'none') ? 'block' : 'none'; var DEFAULT_FONT_SIZE = 12;
}
function urldesc(s) {
return decodeURIComponent(s.replace(/\+/g, " "));
}
function cookie_set(key, value) { function cookie_set(key, value) {
document.cookie = (key+"="+value+"; expires=Sat, 20 Sep 2059 09:05:12; path=/"); document.cookie = (key+"="+value+"; expires=Sat, 20 Sep 2059 09:05:12; path=/");
@@ -42,6 +24,10 @@ function cookie_get(key) {
} }
function highlight(str) { function highlight(str) {
var urldesc = function(s) {
return decodeURIComponent(s.replace(/\+/g, " "));
};
return str return str
.replace(/(\[\d\d\d\d-\d\d-\d\d\s\d\d\:\d\d\:\d\d\] )(\*.+)/g, "$1<span class=\"sys\">$2</span>") .replace(/(\[\d\d\d\d-\d\d-\d\d\s\d\d\:\d\d\:\d\d\] )(\*.+)/g, "$1<span class=\"sys\">$2</span>")
.replace( .replace(
@@ -62,7 +48,7 @@ function highlight(str) {
function fontSize(change) { function fontSize(change) {
var curSize = cookie_get("fontsize"); var curSize = cookie_get("fontsize");
if (curSize === null) { if (curSize === null) {
curSize = 12; curSize = DEFAULT_FONT_SIZE;
} else { } else {
curSize = + curSize; curSize = + curSize;
} }
@@ -71,25 +57,22 @@ function fontSize(change) {
cookie_set("fontsize", curSize); cookie_set("fontsize", curSize);
i("chatarea").style["fontSize"] = ""+curSize+"px"; $chatArea.style["fontSize"] = ""+curSize+"px";
} }
function toggleMenu() { function toggleMenu() {
t(i("tr1")); var $container = document.getElementById("menu-container");
t(i("tr2")); $container.style.display = ($container.style.display == 'block') ? 'none' : 'block';
t(i("spm"));
} }
function highlightLine(no) { function highlightLine(no) {
var lines = i('chatarea').innerHTML.split('<br>'); var lines = $chatArea.innerHTML.split("\n");
lines[no] = '<span class="line-highlighted">' + lines[no] + '</span>'; lines[no] = '<span class="line-highlighted">' + lines[no] + '</span>';
i('chatarea').innerHTML = lines.join('<br>'); $chatArea.innerHTML = lines.join("\n");
} }
var alreadyLoaded = false;
function onLoad() { function onLoad() {
if (alreadyLoaded) { if (alreadyLoaded) {
@@ -99,7 +82,11 @@ function onLoad() {
// //
i('chatarea').innerHTML = highlight(i('chatarea').innerHTML); $chatArea.innerHTML = highlight( $chatArea.innerHTML );
//
document.getElementById("logo").addEventListener("click", toggleMenu);
// //
@@ -108,7 +95,6 @@ function onLoad() {
document.location.hash.substr(0, 6) === '#line-' document.location.hash.substr(0, 6) === '#line-'
) { ) {
highlightLine( parseInt(document.location.hash.substr(6), 10) ); highlightLine( parseInt(document.location.hash.substr(6), 10) );
document.location.hash = '';
} }
// //
@@ -117,60 +103,23 @@ function onLoad() {
// //
i('selHub').onchange = function() { var $form = document.getElementById("search-form");
if ( /\/search\//.test(window.location.pathname) ) { $form.addEventListener("submit", function(ev) {
window.location.pathname = i('selHub').value + "/search/" + encodeURIComponent( i('searchbox').value ); var query = $form.elements["q"].value;
if (query.length === 0) {
} else if ( /\/rx\//.test(window.location.pathname) ) { alert("No search text entered");
window.location.pathname = i('selHub').value + "/rx/" + encodeURIComponent( i('searchbox').value ) ev.preventDefault();
return false;
} else {
i('frmHub').submit();
} }
};
// var prefix = ($form.elements["rx"].checked ? "/rx/" : "/search/");
window.location.hash = "";
window.location.pathname = "/" + $form.elements["h"].value + prefix + encodeURIComponent(query);
Mousetrap.bind('ctrl+alt+h', function() { i("pgprev").click(); }); ev.preventDefault();
Mousetrap.bind('ctrl+alt+l', function() { i("pgnext").click(); }); return false;
Mousetrap.bind('ctrl+alt+j', function() {
YMgoto( YMmod(i("seldate").value, 1) )
});
Mousetrap.bind('ctrl+alt+k', function() {
YMgoto( YMmod(i("seldate").value, -1) )
}); });
Mousetrap.bind('ctrl+alt+m', function() {
i("searchbox").focus();
});
//
}
function YMmod(str, change) {
var t = str.split('-').map(function(x) { return +x; });
t[1] += change;
if (t[1] == 13) {
t[0] += 1;
t[1] = 1;
}
if (t[1] == 0) {
t[0] -= 1;
t[1] = 12;
}
return t.join('-');
}
function YMgoto(str) {
var t = str.split("-");
i("f_y").value = t[0];
i("f_m").value = t[1];
i("seldate").form.submit();
}
function setYM(el) {
YMgoto(el.value);
} }
window.addEventListener('load', onLoad); window.addEventListener('load', onLoad);

View File

@@ -1,16 +0,0 @@
<?php
/*
Chat Archives
`````````````
Requires PHP 5.4 (short-array syntax and ENT_SUBSTITUTE) with short_open_tag
URL rewriting for nginx;
location / {
try_files $uri /index.php?$args;
}
*/
require __DIR__.'/../includes/bootstrap.php';
URLRouter::routeRequest();

View File

@@ -61,6 +61,8 @@ select {
#chatarea { #chatarea {
word-break:break-word; word-break:break-word;
white-space: pre; /* Safari 1/2, IE 6/7 */
white-space: pre-wrap; /* Chrome, Firefox, IE8++ */
} }
.timestamp { .timestamp {
@@ -77,14 +79,6 @@ select {
color: darkgreen; color: darkgreen;
} }
.logo {
width: 102px;
height: 37px;
display: block;
border:0;
padding-bottom: 1.0em;
}
.gt { .gt {
color:#0A0; color:#0A0;
font-weight:bold; font-weight:bold;
@@ -104,11 +98,18 @@ select {
.nav { .nav {
background: #DDD; background: #DDD;
box-shadow: 0px 4px 24px #CCC; box-shadow: 0px 4px 24px #CCC;
font-size:0; /* remove phantom spaces between elements */
} }
.nav form { .nav form {
display: inline; display: inline;
} }
.nav a, .nav .current-page, .nav select {
font-size: 12px;
line-height: 12px;
}
.nav .btn { .nav .btn {
background: white; background: white;
color:black; color:black;
@@ -117,8 +118,14 @@ select {
text-decoration:none; text-decoration:none;
} }
.nav a {
transition:all 0.1s linear;
}
.nav a:hover { .nav a:hover {
border-color: grey black black grey; border-color: grey black black grey;
transition:all 0s linear;
} }
.area-search { .area-search {
@@ -130,16 +137,33 @@ select {
} }
#logo { #logo {
cursor:pointer;
display:inline-block; display:inline-block;
width:16px; width:16px;
height:16px; height:16px;
background:transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAbtJREFUOMt9kb1qAlEQhacJ6QJBTJEiTSDpEqw1jYWIpQ9gIYIWghamELGysRE0Eo0GRRQrCyslIKIi/hUawVIEfYW8wGTOjbssyZqFbznMOeeyO5cOhwOdotls+sB/mZNGvV73tdttBtAnD9jv96aUy+XNZDJhAH0qR7vd7g+VSuWuVCrxZrNRQGNmlqXtdkvFYtFeKBR8uVzuTWgIH/j01WqlgMbs6L0hiw66JAN7NpvlfD7P1WoV/8uyOB4OhzydThXQmMFDBll00KVMJvOYSqV4Pp/zbDbjfr//L8ggiw66tF6vKZlMvkejUe71emppnU7HFHjIIIsOurRcLhWJROI5HA7v8KmtVssUeMggq/VosVjoiGEPBoNcq9VMgYeMsUOyJJ1QKPQai8VYNqyurtvtKqAxg4eMsUPj8VjH6/V+xuNxbjQaamHpdFoBjRk8ZIwdGo1GOg6H40u7Lo/Hg/AEQGvXi4yxQ4PBQBGJRC5sNhv7/X52u93bQCAQ1TxozOAhg6zm/byIboVHi8XidLlcr6KdgucXTjnkxWq1Pol+QEd15TQdec4Ei3AlXAv3R26Os0vh3Nj5Bg36USLeWI8KAAAAAElFTkSuQmCC') no-repeat 0 0;
margin-left:2px; margin-left:2px;
margin-right:4px; margin-right:4px;
} }
/* Utility class */
.noselect {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome and Opera */
}
/* Dropdown */ /* Dropdown */
#menu-container {
display:none; /* default */
}
.ddmenu { .ddmenu {
display:block; display:block;
position:absolute; position:absolute;
@@ -165,8 +189,6 @@ select {
.ddmenu a:hover { .ddmenu a:hover {
background:#FFF; background:#FFF;
-moz-transition:all 0.1s linear;
-webkit-transition:all 0.1s linear;
} }
#tr1 { #tr1 {
@@ -202,7 +224,7 @@ select {
text-align:center; text-align:center;
} }
.nav, .nav select { .nav a, .nav .current-page, .nav select {
font-size: 16px; font-size: 16px;
line-height:16px; line-height:16px;
} }
@@ -227,12 +249,29 @@ select {
} }
.area-search { .area-search {
display:block;
float:none; float:none;
margin-top: 4px;
} }
#searchbox { #searchbox {
width:auto; width:auto;
} }
ul.search-results {
list-style-type: none;
padding-left: 2px;
}
.search-results li a:first-child {
/* convert mini link to touchable */
display:inline-block;
padding: 2px 6px;
margin: 2px;
border: 1px solid grey;
background: lightgrey;
}
} }
@media (max-width: 400px) { @media (max-width: 400px) {

File diff suppressed because one or more lines are too long

View File

@@ -25,7 +25,7 @@ func (this *URLHelper) intval(sz string) int {
} }
func (this *URLHelper) get(sz string) string { func (this *URLHelper) get(sz string) string {
return this.r.URL.Query().Get(sz) return this.r.URL.Query().Get(sz) // n.b. automatically unescaped
} }
func (this *URLHelper) hasGet(sz string) bool { func (this *URLHelper) hasGet(sz string) bool {