Experimenting with Nox's Control Panel.

Experimenting with better cache busting for static resources.
HTTPSRedirect requests are now counted in the route analytics.
More scripts are loaded asynchronously now.
Upped the default ReadTimeout to eight seconds.
Reduce the number of unneccesary NewAcc calls.
Added panel_before_head as an injection point for themes.
Themes can now declare scripts to be loaded asynchronously.
Tweaked the WS resumption algorithm to mae the backoffs a little less aggressive.
Fixed an ordering issue in the WS resumption algorithm where backoffs weren't expiring as fast as they should have.
Fixed a bug where template logs weren't being written due to a panic.
You can now use byte slices in more places in the transpiled templates.
Fixed a bug where Cosora's misc.js seemed to be erroring out.
Fixed a bug where YT embeds were getting blocked by the CSP.

Added the panel_back_to_site phrase.
Added the panel_welcome phrase.
This commit is contained in:
Azareal 2019-03-22 08:59:41 +10:00
parent 3320cb4697
commit 660f24acff
74 changed files with 375 additions and 201 deletions

View File

@ -96,12 +96,11 @@ var langCodes = []string{
type DefaultLangViewCounter struct {
buckets []*RWMutexCounterBucket //[OSID]count
codesToIndices map[string]int
insert *sql.Stmt
insert *sql.Stmt
}
func NewDefaultLangViewCounter() (*DefaultLangViewCounter, error) {
acc := qgen.NewAcc()
func NewDefaultLangViewCounter(acc *qgen.Accumulator) (*DefaultLangViewCounter, error) {
var langBuckets = make([]*RWMutexCounterBucket, len(langCodes))
for bucketID, _ := range langBuckets {
langBuckets[bucketID] = &RWMutexCounterBucket{counter: 0}

View File

@ -12,8 +12,7 @@ type DefaultRouteViewCounter struct {
insert *sql.Stmt
}
func NewDefaultRouteViewCounter() (*DefaultRouteViewCounter, error) {
acc := qgen.NewAcc()
func NewDefaultRouteViewCounter(acc *qgen.Accumulator) (*DefaultRouteViewCounter, error) {
var routeBuckets = make([]*RWMutexCounterBucket, len(routeMapEnum))
for bucketID, _ := range routeBuckets {
routeBuckets[bucketID] = &RWMutexCounterBucket{counter: 0}

View File

@ -14,8 +14,7 @@ type DefaultOSViewCounter struct {
insert *sql.Stmt
}
func NewDefaultOSViewCounter() (*DefaultOSViewCounter, error) {
acc := qgen.NewAcc()
func NewDefaultOSViewCounter(acc *qgen.Accumulator) (*DefaultOSViewCounter, error) {
var osBuckets = make([]*RWMutexCounterBucket, len(osMapEnum))
for bucketID, _ := range osBuckets {
osBuckets[bucketID] = &RWMutexCounterBucket{counter: 0}

View File

@ -27,7 +27,7 @@ var staticFileMutex sync.RWMutex
type SFile struct {
Data []byte
GzipData []byte
Sha256 []byte
Sha256 string
Pos int64
Length int64
GzipLength int64
@ -240,7 +240,7 @@ func (list SFileList) JSTmplInit() error {
// Get a checksum for CSPs and cache busting
hasher := sha256.New()
hasher.Write(data)
checksum := []byte(hex.EncodeToString(hasher.Sum(nil)))
checksum := hex.EncodeToString(hasher.Sum(nil))
list.Set("/static/"+path, SFile{data, gzipData, checksum, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
@ -267,7 +267,7 @@ func (list SFileList) Init() error {
// Get a checksum for CSPs and cache busting
hasher := sha256.New()
hasher.Write(data)
checksum := []byte(hex.EncodeToString(hasher.Sum(nil)))
checksum := hex.EncodeToString(hasher.Sum(nil))
// Avoid double-compressing images
var gzipData []byte
@ -318,7 +318,7 @@ func (list SFileList) Add(path string, prefix string) error {
// Get a checksum for CSPs and cache busting
hasher := sha256.New()
hasher.Write(data)
checksum := []byte(hex.EncodeToString(hasher.Sum(nil)))
checksum := hex.EncodeToString(hasher.Sum(nil))
list.Set("/static"+path, SFile{data, gzipData, checksum, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})

View File

@ -10,14 +10,20 @@ import (
"github.com/Azareal/Gosora/common/phrases"
)
type HResource struct {
Name string
Hash string
}
// TODO: Allow resources in spots other than /static/ and possibly even external domains (e.g. CDNs)
// TODO: Preload Trumboyg on Cosora on the forum list
type Header struct {
Title string
//Title []byte // Experimenting with []byte for increased efficiency, let's avoid converting too many things to []byte, as it involves a lot of extra boilerplate
NoticeList []string
Scripts []string
PreScriptsAsync []string
Scripts []HResource
PreScriptsAsync []HResource
ScriptsAsync []HResource
//Preload []string
Stylesheets []string
Widgets PageWidgets
@ -44,11 +50,41 @@ type Header struct {
}
func (header *Header) AddScript(name string) {
header.Scripts = append(header.Scripts, name)
fname := "/static/" + name
var hash string
if fname[0] == '/' && fname[1] != '/' {
file, ok := StaticFiles.Get(fname)
if ok {
hash = file.Sha256
}
}
//log.Print("name:", name)
//log.Print("hash:", hash)
header.Scripts = append(header.Scripts, HResource{name, hash})
}
func (header *Header) AddPreScriptAsync(name string) {
header.PreScriptsAsync = append(header.PreScriptsAsync, name)
fname := "/static/" + name
var hash string
if fname[0] == '/' && fname[1] != '/' {
file, ok := StaticFiles.Get(fname)
if ok {
hash = file.Sha256
}
}
header.PreScriptsAsync = append(header.PreScriptsAsync, HResource{name, hash})
}
func (header *Header) AddScriptAsync(name string) {
fname := "/static/" + name
var hash string
if fname[0] == '/' && fname[1] != '/' {
file, ok := StaticFiles.Get(fname)
if ok {
hash = file.Sha256
}
}
header.ScriptsAsync = append(header.ScriptsAsync, HResource{name, hash})
}
/*func (header *Header) Preload(name string) {

View File

@ -131,7 +131,11 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
if ext == "css" {
header.AddSheet(resource.Name)
} else if ext == "js" {
header.AddScript(resource.Name)
if resource.Async {
header.AddScriptAsync(resource.Name)
} else {
header.AddScript(resource.Name)
}
}
}
}
@ -229,7 +233,11 @@ func userCheck(w http.ResponseWriter, r *http.Request, user *User) (header *Head
if ext == "css" {
header.AddSheet(resource.Name)
} else if ext == "js" {
header.AddScript(resource.Name)
if resource.Async {
header.AddScriptAsync(resource.Name)
} else {
header.AddScript(resource.Name)
}
}
}
}

View File

@ -109,8 +109,9 @@ func tmplInitHeaders(user User, user2 User, user3 User) (*Header, *Header, *Head
CurrentUser: user,
NoticeList: []string{"test"},
Stylesheets: []string{"panel.css"},
Scripts: []string{"whatever.js"},
PreScriptsAsync: []string{"whatever.js"},
Scripts: []HResource{HResource{"whatever.js", "d"}},
PreScriptsAsync: []HResource{HResource{"whatever.js", "d"}},
ScriptsAsync: []HResource{HResource{"whatever.js", "d"}},
Widgets: PageWidgets{
LeftSidebar: template.HTML("lalala"),
},

View File

@ -2,11 +2,13 @@ package tmpl
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"reflect"
"runtime/debug"
"strconv"
"strings"
"text/template/parse"
@ -70,7 +72,8 @@ type CTemplateSet struct {
themeName string
perThemeTmpls map[string]bool
logger *log.Logger
logger *log.Logger
loggerf *os.File
}
func NewCTemplateSet(in string) *CTemplateSet {
@ -110,7 +113,8 @@ func NewCTemplateSet(in string) *CTemplateSet {
"dyntmpl": true,
"index": true,
},
logger: log.New(f, "", log.LstdFlags),
logger: log.New(f, "", log.LstdFlags),
loggerf: f,
}
}
@ -155,6 +159,7 @@ func (c *CTemplateSet) ResetLogs(in string) {
panic(err)
}
c.logger = log.New(f, "", log.LstdFlags)
c.loggerf = f
}
type SkipBlock struct {
@ -268,6 +273,19 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe
}
func (c *CTemplateSet) compile(name string, content string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (out string, err error) {
defer func() {
r := recover()
if r != nil {
fmt.Println(r)
debug.PrintStack()
err := c.loggerf.Sync()
if err != nil {
fmt.Println(err)
}
log.Fatal("")
return
}
}()
//c.dumpCall("compile", name, content, expects, expectsInt, varList, imports)
//c.detailf("c: %+v\n", c)
c.importMap = map[string]string{}
@ -1460,6 +1478,16 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V
c.addText(con, []byte("false"))
con.Push("endelse", "}\n")
return
case reflect.Slice:
if val.Len() == 0 {
c.critical("varname:", varname)
panic("The sample data needs at-least one or more elements for the slices. We're looking into removing this requirement at some point!")
}
item := val.Index(0)
if item.Type().Name() != "uint8" { // uint8 == byte, complicated because it's a type alias
panic("unable to format " + item.Type().Name() + " as text")
}
base = varname
case reflect.String:
if val.Type().Name() != "string" && !strings.HasPrefix(varname, "string(") {
varname = "string(" + varname + ")"

View File

@ -77,6 +77,7 @@ type ThemeResource struct {
Name string
Location string
Loggedin bool // Only serve this resource to logged in users
Async bool
}
type ThemeMapTmplToDock struct {
@ -162,7 +163,7 @@ func (theme *Theme) AddThemeStaticFiles() error {
// Get a checksum for CSPs and cache busting
hasher := sha256.New()
hasher.Write(data)
checksum := []byte(hex.EncodeToString(hasher.Sum(nil)))
checksum := hex.EncodeToString(hasher.Sum(nil))
StaticFiles.Set("/static/"+theme.Name+path, SFile{data, gzipData, checksum, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})

View File

@ -92,6 +92,12 @@ func RouteWebsockets(w http.ResponseWriter, r *http.Request, user User) RouteErr
currentPage = string(msgblocks[1])
wsPageResponses(wsUser, conn, currentPage)
}
} else if bytes.HasPrefix(msg, []byte("resume ")) {
msgblocks := bytes.SplitN(msg, []byte(" "), 2)
if len(msgblocks) < 2 {
continue
}
//log.Print("resuming on " + string(msgblocks[1]))
}
/*if bytes.Equal(message,[]byte(`start-view`)) {
} else if bytes.Equal(message,[]byte(`end-view`)) {

View File

@ -84,7 +84,7 @@ MaxTopicTitleLength - The maximum length that a topic can be. Please note that t
MaxUsernameLength - The maximum length that a user's name can be. Please note that this measures the number of bytes and may differ from language to language with it being equal to a letter in English and being two bytes in others.
ReadTimeout - The number of seconds that we are allowed to take to fully read a request. Defaults to 5.
ReadTimeout - The number of seconds that we are allowed to take to fully read a request. Defaults to 8.
WriteTimeout - The number of seconds that a route is allowed to run for before the request is automatically terminated. Defaults to 10.

View File

@ -164,6 +164,7 @@ var RouteMap = map[string]interface{}{
"routes.RobotsTxt": routes.RobotsTxt,
"routes.SitemapXml": routes.SitemapXml,
"routes.BadRoute": routes.BadRoute,
"routes.HTTPSRedirect": routes.HTTPSRedirect,
}
// ! NEVER RELY ON THESE REMAINING THE SAME BETWEEN COMMITS
@ -309,6 +310,7 @@ var routeMapEnum = map[string]int{
"routes.RobotsTxt": 138,
"routes.SitemapXml": 139,
"routes.BadRoute": 140,
"routes.HTTPSRedirect": 141,
}
var reverseRouteMapEnum = map[int]string{
0: "routes.Overview",
@ -452,6 +454,7 @@ var reverseRouteMapEnum = map[int]string{
138: "routes.RobotsTxt",
139: "routes.SitemapXml",
140: "routes.BadRoute",
141: "routes.HTTPSRedirect",
}
var osMapEnum = map[string]int{
"unknown": 0,
@ -600,6 +603,17 @@ func (writ *WriterIntercept) WriteHeader(code int) {
writ.ResponseWriter.WriteHeader(code)
}
// HTTPSRedirect is a connection handler which redirects all HTTP requests to HTTPS
type HTTPSRedirect struct {
}
func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Connection", "close")
counters.RouteViewCounter.Bump(141)
dest := "https://" + req.Host + req.URL.String()
http.Redirect(w, req, dest, http.StatusTemporaryRedirect)
}
type GenRouter struct {
UploadHandler func(http.ResponseWriter, *http.Request)
extraRoutes map[string]func(http.ResponseWriter, *http.Request, common.User) common.RouteError

View File

@ -708,6 +708,8 @@
"option_yes":"Yes",
"option_no":"No",
"panel_back_to_site":"Back to Site",
"panel_welcome":"Welcome ",
"panel_menu_head":"Control Panel",
"panel_menu_aria":"The control panel menu",
"panel_menu_users":"Users",

11
main.go
View File

@ -28,7 +28,6 @@ import (
"github.com/Azareal/Gosora/common/counters"
"github.com/Azareal/Gosora/common/phrases"
"github.com/Azareal/Gosora/query_gen"
"github.com/Azareal/Gosora/routes"
"github.com/fsnotify/fsnotify"
"github.com/pkg/errors"
)
@ -181,15 +180,15 @@ func afterDBInit() (err error) {
if err != nil {
return errors.WithStack(err)
}
counters.OSViewCounter, err = counters.NewDefaultOSViewCounter()
counters.OSViewCounter, err = counters.NewDefaultOSViewCounter(acc)
if err != nil {
return errors.WithStack(err)
}
counters.LangViewCounter, err = counters.NewDefaultLangViewCounter()
counters.LangViewCounter, err = counters.NewDefaultLangViewCounter(acc)
if err != nil {
return errors.WithStack(err)
}
counters.RouteViewCounter, err = counters.NewDefaultRouteViewCounter()
counters.RouteViewCounter, err = counters.NewDefaultRouteViewCounter(acc)
if err != nil {
return errors.WithStack(err)
}
@ -472,7 +471,7 @@ func startServer() {
var newServer = func(addr string, handler http.Handler) *http.Server {
rtime := common.Config.ReadTimeout
if rtime == 0 {
rtime = 5
rtime = 8
} else if rtime == -1 {
rtime = 0
}
@ -527,7 +526,7 @@ func startServer() {
// TODO: Redirect to port 443
go func() {
log.Print("Listening on port 80")
common.StoppedServer(newServer(":80", &routes.HTTPSRedirect{}).ListenAndServe())
common.StoppedServer(newServer(":80", &HTTPSRedirect{}).ListenAndServe())
}()
}
log.Printf("Listening on port %s", common.Site.Port)

View File

@ -59,4 +59,6 @@ function buildStatsChart(rawLabels, seriesData, timeRange, legendNames) {
labels: labels,
series: seriesData,
}, config);
}
}
runInitHook("analytics_loaded");

View File

@ -193,7 +193,7 @@ function wsAlertEvent(data) {
updateAlertList(generalAlerts/*, alist*/);
}
function runWebSockets() {
function runWebSockets(resume = false) {
if(window.location.protocol == "https:") {
conn = new WebSocket("wss://" + document.location.host + "/ws/");
} else conn = new WebSocket("ws://" + document.location.host + "/ws/");
@ -206,6 +206,7 @@ function runWebSockets() {
conn.onopen = () => {
console.log("The WebSockets connection was opened");
conn.send("page " + document.location.pathname + '\r');
if(resume) conn.send("resume " + Math.round((new Date()).getTime() / 1000) + '\r');
// TODO: Don't ask again, if it's denied. We could have a setting in the UCP which automatically requests this when someone flips desktop notifications on
if(me.User.ID > 0) Notification.requestPermission();
}
@ -213,23 +214,22 @@ function runWebSockets() {
conn.onclose = () => {
conn = false;
console.log("The WebSockets connection was closed");
let backoff = 1000;
let backoff = 0.8;
if(wsBackoff < 0) wsBackoff = 0;
else if(wsBackoff > 12) backoff = 13000;
else if(wsBackoff > 5) backoff = 7000;
else if(wsBackoff > 12) backoff = 11;
else if(wsBackoff > 5) backoff = 5;
wsBackoff++;
setTimeout(() => {
var alertMenuList = document.getElementsByClassName("menu_alerts");
for(var i = 0; i < alertMenuList.length; i++) {
loadAlerts(alertMenuList[i]);
}
runWebSockets();
}, 60 * backoff);
for(var i = 0; i < alertMenuList.length; i++) loadAlerts(alertMenuList[i]);
runWebSockets(true);
}, backoff * 60 * 1000);
if(wsBackoff > 0) {
if(wsBackoff <= 5) setTimeout(() => wsBackoff--, 60 * 4000);
else if(wsBackoff <= 12) setTimeout(() => wsBackoff--, 60 * 20000);
if(wsBackoff <= 5) setTimeout(() => wsBackoff--, 5.5 * 60 * 1000);
else if(wsBackoff <= 12) setTimeout(() => wsBackoff--, 11.5 * 60 * 1000);
else setTimeout(() => wsBackoff--, 20 * 60 * 1000);
}
}
@ -333,16 +333,14 @@ function runWebSockets() {
notifyOnScriptW("template_alert", (e) => {
if(e!=undefined) console.log("failed alert? why?", e)
}, () => {
console.log("ha")
//console.log("ha")
if(!Template_alert) throw("template function not found");
addInitHook("after_phrases", () => {
// TODO: The load part of loadAlerts could be done asynchronously while the update of the DOM could be deferred
$(document).ready(() => {
alertsInitted = true;
var alertMenuList = document.getElementsByClassName("menu_alerts");
for(var i = 0; i < alertMenuList.length; i++) {
loadAlerts(alertMenuList[i]);
}
for(var i = 0; i < alertMenuList.length; i++) loadAlerts(alertMenuList[i]);
if(window["WebSocket"]) runWebSockets();
});
});

View File

@ -4,7 +4,7 @@ var me = {};
var phraseBox = {};
if(tmplInits===undefined) var tmplInits = {};
var tmplPhrases = []; // [key] array of phrases indexed by order of use
var hooks = {
var hooks = { // Shorten this list by binding the hooks just in time?
"pre_iffe": [],
"pre_init": [],
"start_init": [],
@ -15,6 +15,7 @@ var hooks = {
"open_edit":[],
"close_edit":[],
"edit_item_pre_bind":[],
"analytics_loaded":[],
};
var ranInitHooks = {}

View File

@ -174,6 +174,7 @@ func main() {
mapIt("routes.RobotsTxt")
mapIt("routes.SitemapXml")
mapIt("routes.BadRoute")
mapIt("routes.HTTPSRedirect")
tmplVars.AllRouteNames = allRouteNames
tmplVars.AllRouteMap = allRouteMap
@ -381,6 +382,17 @@ func (writ *WriterIntercept) WriteHeader(code int) {
writ.ResponseWriter.WriteHeader(code)
}
// HTTPSRedirect is a connection handler which redirects all HTTP requests to HTTPS
type HTTPSRedirect struct {
}
func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Connection", "close")
counters.RouteViewCounter.Bump({{ index .AllRouteMap "routes.HTTPSRedirect" }})
dest := "https://" + req.Host + req.URL.String()
http.Redirect(w, req, dest, http.StatusTemporaryRedirect)
}
type GenRouter struct {
UploadHandler func(http.ResponseWriter, *http.Request)
extraRoutes map[string]func(http.ResponseWriter, *http.Request, common.User) common.RouteError

View File

@ -29,8 +29,9 @@ func renderTemplate(tmplName string, w http.ResponseWriter, r *http.Request, hea
}
// TODO: Expand this to non-HTTPS requests too
if !header.LooseCSP && common.Site.EnableSsl {
w.Header().Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; upgrade-insecure-requests")
w.Header().Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self' www.youtube-nocookie.com;upgrade-insecure-requests")
}
header.AddScript("global.js")
if header.CurrentUser.IsAdmin {
header.Elapsed1 = time.Since(header.StartedAt).String()
}

View File

@ -125,7 +125,7 @@ func PreAnalyticsDetail(w http.ResponseWriter, r *http.Request, user *common.Use
}
basePage.AddSheet("chartist/chartist.min.css")
basePage.AddScript("chartist/chartist.min.js")
basePage.AddScript("analytics.js")
basePage.AddScriptAsync("analytics.js")
return basePage, nil
}

View File

@ -22,6 +22,7 @@ func successRedirect(dest string, w http.ResponseWriter, r *http.Request, isJs b
}
func renderTemplate(tmplName string, w http.ResponseWriter, r *http.Request, header *common.Header, pi interface{}) common.RouteError {
header.AddScript("global.js")
if common.RunPreRenderHook("pre_render_"+tmplName, w, r, &header.CurrentUser, pi) {
return nil
}

View File

@ -1,17 +1,5 @@
package routes
import "net/http"
// HTTPSRedirect is a connection handler which redirects all HTTP requests to HTTPS
type HTTPSRedirect struct {
}
func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Connection", "close")
dest := "https://" + req.Host + req.URL.String()
http.Redirect(w, req, dest, http.StatusTemporaryRedirect)
}
// Temporary stubs for view tracking
func DynamicRoute() {
}
@ -19,3 +7,7 @@ func UploadedFile() {
}
func BadRoute() {
}
// Real implementation is in router_gen/main.go, this is just a stub to map the analytics onto
func HTTPSRedirect() {
}

View File

@ -6,13 +6,14 @@
{{range .Header.Stylesheets}}
<link href="/static/{{.}}" rel="stylesheet" type="text/css">{{end}}
{{range .Header.PreScriptsAsync}}
<script async type="text/javascript" src="/static/{{.}}"></script>{{end}}
<script async type="text/javascript" src="/static/{{.Name}}{{if .Hash}}?h={{.Hash}}{{end}}"></script>{{end}}
<meta property="x-loggedin" content="{{.CurrentUser.Loggedin}}" />
<script type="text/javascript" src="/static/init.js"></script>
{{range .Header.ScriptsAsync}}
<script async type="text/javascript" src="/static/{{.Name}}{{if .Hash}}?h={{.Hash}}{{end}}"></script>{{end}}
<script type="text/javascript" src="/static/jquery-3.1.1.min.js"></script>
{{range .Header.Scripts}}
<script type="text/javascript" src="/static/{{.}}"></script>{{end}}
<script type="text/javascript" src="/static/global.js"></script>
<script type="text/javascript" src="/static/{{.Name}}{{if .Hash}}?h={{.Hash}}{{end}}"></script>{{end}}
<meta name="viewport" content="width=device-width,initial-scale = 1.0, maximum-scale=1.0,user-scalable=no" />
{{if .Header.MetaDesc}}<meta name="description" content="{{.Header.MetaDesc}}" />{{end}}
{{/** TODO: Have page / forum / topic level tags and descriptions below as-well **/}}

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_logs_administration_head"}}</h1></div>
</div>

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/agent/{{.Agent}}" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/agents/" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/forum/{{.Agent}}" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/forums/" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/lang/{{.Agent}}" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/langs/" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/posts/" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/referrer/{{.Agent}}" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/referrers/" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/route/{{.Route}}" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/routes/" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">

View File

@ -11,6 +11,10 @@ let legendNames = [{{range .Graph.Legends}}
{{.}},{{end}}
];
addInitHook("after_phrases", () => {
buildStatsChart(rawLabels, seriesData, "{{.TimeRange}}",legendNames);
addInitHook("end_init", () => {
addInitHook("analytics_loaded", () => {
buildStatsChart(rawLabels, seriesData, "{{.TimeRange}}",legendNames);
});
});
});
</script>

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/system/{{.Agent}}" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/systems/" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/topics/" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/views/" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "areyousure_head"}}</h1></div>
</div>

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_backups_head"}}</h1></div>
</div>

View File

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_dashboard_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_dashboard_head"}}</h1></div>
</div>

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main id="panel_dashboard_right" class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_debug_head"}}</h1></div>
</div>

View File

@ -7,6 +7,7 @@ var formVars = {'perm_preset': ['can_moderate','can_post','read_only','no_access
</script>
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{.Name}}{{lang "panel_forum_head_suffix"}}</h1></div>
</div>

View File

@ -3,6 +3,7 @@
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{.Name}}{{lang "panel_forum_head_suffix"}}</h1></div>
</div>

View File

@ -8,6 +8,7 @@
</script>
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_forums_head"}}</h1></div>
</div>

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_group_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{.Name}}{{lang "panel_group_head_suffix"}}</h1></div>
</div>

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_group_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{.Name}}{{lang "panel_group_head_suffix"}}</h1></div>
</div>

View File

@ -3,6 +3,7 @@
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_groups_head"}}</h1></div>
</div>

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_logs_moderation_head"}}</h1></div>
</div>

View File

@ -2,6 +2,7 @@
<div id="panel_page_list" class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_pages_head"}}</h1></div>
</div>

View File

@ -2,6 +2,7 @@
<div id="panel_page_edit" class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_pages_edit_head"}}</h1></div>
</div>

View File

@ -3,6 +3,7 @@
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_plugins_head"}}</h1></div>
</div>

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_logs_registration_head"}}</h1></div>
</div>

View File

@ -3,6 +3,7 @@
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{.Setting.FriendlyName}}</h1></div>
</div>

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_settings_head"}}</h1></div>
</div>

View File

@ -11,6 +11,7 @@
</style>
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_themes_primary_themes"}}</h1></div>
</div>

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_themes_menus_head"}}</h1></div>
</div>

View File

@ -4,6 +4,7 @@
{{/** TODO: Write the backend code and JS code for saving this menu **/}}
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_themes_menus_edit_head"}}</h1></div>
</div>

View File

@ -2,6 +2,7 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_themes_menus_items_head"}}</h1></div>
</div>

View File

@ -13,6 +13,7 @@ type Widget struct {
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_themes_widgets_head"}}</h1></div>
</div>

View File

@ -3,6 +3,7 @@
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_user_head"}}</h1></div>
</div>

View File

@ -3,6 +3,7 @@
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_users_head"}}</h1></div>
</div>

View File

@ -3,6 +3,7 @@
{{template "panel_menu.html" . }}
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_word_filters_head"}}</h1></div>
</div>

View File

@ -1,116 +1,113 @@
"use strict"
"use strict";
$(document).ready(function(){
let loggedIn = document.head.querySelector("[property='x-loggedin']").content;
if(loggedIn) {
// Is there we way we can append instead? Maybe, an editor plugin?
attachItemCallback = function(attachItem) {
let currentContent = $('#input_content').trumbowyg('html');
$('#input_content').trumbowyg('html', currentContent);
}
$(".topic_name_row").click(function(){
$(".topic_create_form").addClass("selectedInput");
});
//$.trumbowyg.svgPath = false;
(() => {
console.log("bf")
addInitHook("end_init", () => {
console.log("af")
let loggedIn = document.head.querySelector("[property='x-loggedin']").content;
if(loggedIn) {
// Is there we way we can append instead? Maybe, an editor plugin?
attachItemCallback = function(attachItem) {
let currentContent = $('#input_content').trumbowyg('html');
$('#input_content').trumbowyg('html', currentContent);
}
$(".topic_name_row").click(function(){
$(".topic_create_form").addClass("selectedInput");
});
//$.trumbowyg.svgPath = false;
// TODO: Bind this to the viewport resize event
var btnlist = [];
if(document.documentElement.clientWidth > 550) {
btnlist = [['viewHTML'],['undo','redo'],['formatting'],['strong','em','del'],['link'],['insertImage'],['unorderedList','orderedList'],['removeformat']];
} else {
btnlist = [['viewHTML'],['strong','em','del'],['link'],['insertImage'],['unorderedList','orderedList'],['removeformat']];
}
$('.topic_create_form #input_content').trumbowyg({
btns: btnlist,
});
$('.topic_reply_form #input_content').trumbowyg({
btns: btnlist,
autogrow: true,
});
$('#profile_comments_form .topic_reply_form .input_content').trumbowyg({
btns: [['viewHTML'],['strong','em','del'],['link'],['insertImage'],['removeformat']],
autogrow: true,
});
addHook("edit_item_pre_bind", () => {
$('.user_content textarea').trumbowyg({
// TODO: Bind this to the viewport resize event
var btnlist = [];
if(document.documentElement.clientWidth > 550) {
btnlist = [['viewHTML'],['undo','redo'],['formatting'],['strong','em','del'],['link'],['insertImage'],['unorderedList','orderedList'],['removeformat']];
} else {
btnlist = [['viewHTML'],['strong','em','del'],['link'],['insertImage'],['unorderedList','orderedList'],['removeformat']];
}
$('.topic_create_form #input_content').trumbowyg({
btns: btnlist,
});
$('.topic_reply_form #input_content').trumbowyg({
btns: btnlist,
autogrow: true,
});
});
}
// TODO: Refactor this to use `each` less
$('.button_menu').click(function(){
console.log(".button_menu");
// The outer container
let buttonPane = newElement("div","button_menu_pane");
let postItem = $(this).parents('.post_item');
// Create the userinfo row in the pane
let userInfo = newElement("div","userinfo");
postItem.find('.avatar_item').each(function(){
userInfo.appendChild(this);
});
let userText = newElement("div","userText");
postItem.find('.userinfo:not(.avatar_item)').children().each(function(){
userText.appendChild(this);
});
userInfo.appendChild(userText);
buttonPane.appendChild(userInfo);
// Copy a short preview of the post contents into the pane
postItem.find('.user_content').each(function(){
// TODO: Truncate an excessive number of lines to 5 or so
let contents = this.innerHTML;
if(contents.length > 45) {
this.innerHTML = contents.substring(0,45) + "...";
}
buttonPane.appendChild(this);
});
// Copy the buttons from the post to the pane
let buttonGrid = newElement("div","buttonGrid");
let gridElementCount = 0;
$(this).parent().children('a:not(.button_menu)').each(function(){
buttonGrid.appendChild(this);
gridElementCount++;
});
// Fill in the placeholder grid nodes
let rowCount = 4;
console.log("rowCount: ",rowCount);
console.log("gridElementCount: ",gridElementCount);
if(gridElementCount%rowCount != 0) {
let fillerNodes = (rowCount - (gridElementCount%rowCount));
console.log("fillerNodes: ",fillerNodes);
for(let i = 0; i < fillerNodes;i++ ) {
console.log("added a gridFiller");
buttonGrid.appendChild(newElement("div","gridFiller"));
}
$('#profile_comments_form .topic_reply_form .input_content').trumbowyg({
btns: [['viewHTML'],['strong','em','del'],['link'],['insertImage'],['removeformat']],
autogrow: true,
});
addHook("edit_item_pre_bind", () => {
$('.user_content textarea').trumbowyg({
btns: btnlist,
autogrow: true,
});
});
}
buttonPane.appendChild(buttonGrid);
document.getElementById("back").appendChild(buttonPane);
// TODO: Refactor this to use `each` less
$('.button_menu').click(function(){
console.log(".button_menu");
// The outer container
let buttonPane = newElement("div","button_menu_pane");
let postItem = $(this).parents('.post_item');
// Create the userinfo row in the pane
let userInfo = newElement("div","userinfo");
postItem.find('.avatar_item').each(function(){
userInfo.appendChild(this);
});
let userText = newElement("div","userText");
postItem.find('.userinfo:not(.avatar_item)').children().each(function(){
userText.appendChild(this);
});
userInfo.appendChild(userText);
buttonPane.appendChild(userInfo);
// Copy a short preview of the post contents into the pane
postItem.find('.user_content').each(function(){
// TODO: Truncate an excessive number of lines to 5 or so
let contents = this.innerHTML;
if(contents.length > 45) this.innerHTML = contents.substring(0,45) + "...";
buttonPane.appendChild(this);
});
// Copy the buttons from the post to the pane
let buttonGrid = newElement("div","buttonGrid");
let gridElementCount = 0;
$(this).parent().children('a:not(.button_menu)').each(function(){
buttonGrid.appendChild(this);
gridElementCount++;
});
// Fill in the placeholder grid nodes
let rowCount = 4;
console.log("rowCount: ",rowCount);
console.log("gridElementCount: ",gridElementCount);
if(gridElementCount%rowCount != 0) {
let fillerNodes = (rowCount - (gridElementCount%rowCount));
console.log("fillerNodes: ",fillerNodes);
for(let i = 0; i < fillerNodes;i++ ) {
console.log("added a gridFiller");
buttonGrid.appendChild(newElement("div","gridFiller"));
}
}
buttonPane.appendChild(buttonGrid);
document.getElementById("back").appendChild(buttonPane);
});
// Move the alerts under the first header
let colSel = $(".colstack_right .colstack_head:first");
let colSelAlt = $(".colstack_right .colstack_item:first");
let colSelAltAlt = $(".colstack_right .coldyn_block:first");
if(colSel.length > 0) $('.alert').insertAfter(colSel);
else if (colSelAlt.length > 0) $('.alert').insertBefore(colSelAlt);
else if (colSelAltAlt.length > 0) $('.alert').insertBefore(colSelAltAlt);
else $('.alert').insertAfter(".rowhead:first");
});
// Move the alerts under the first header
let colSel = $(".colstack_right .colstack_head:first");
let colSelAlt = $(".colstack_right .colstack_item:first");
let colSelAltAlt = $(".colstack_right .coldyn_block:first");
if(colSel.length > 0) {
$('.alert').insertAfter(colSel);
} else if (colSelAlt.length > 0) {
$('.alert').insertBefore(colSelAlt);
} else if (colSelAltAlt.length > 0) {
$('.alert').insertBefore(colSelAltAlt);
} else {
$('.alert').insertAfter(".rowhead:first");
}
});
})();
function newElement(etype, eclass) {
let element = document.createElement(etype);

View File

@ -29,7 +29,8 @@
},
{
"Name":"cosora/misc.js",
"Location":"global"
"Location":"global",
"Async":true
}
]
}

View File

@ -0,0 +1,6 @@
<div class="above_right">
<div class="left_bit">{{lang "panel_back_to_site"}}</div>
<div class="right_bit">
<img src="{{.CurrentUser.MicroAvatar}}" height=32 width=32 />
<span>{{lang "panel_welcome"}}{{.CurrentUser.Name}}</span></div>
</div>

View File

@ -1,7 +1,7 @@
<nav class="colstack_left" aria-label="{{lang "panel_menu_aria"}}">
<div class="colstack_item colstack_head">
<!--<div class="colstack_item colstack_head">
<div class="rowitem back_to_site"><a href="/">Back to site</a></div>
</div>
</div>-->
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/groups/edit/{{.ID}}">{{lang "panel_group_menu_head"}}</a></div>
</div>

View File

@ -1,5 +1,5 @@
<nav class="colstack_left" aria-label="{{lang "panel_menu_aria"}}">
<div class="colstack_item colstack_head">
<!--<div class="colstack_item colstack_head">
<div class="rowitem back_to_site"><a href="/">Back to site</a></div>
</div>
</div>-->
{{template "panel_inner_menu.html" . }}</nav>

View File

@ -15,29 +15,24 @@
});
addHook("open_edit", () => $('.topic_block').addClass("edithead"));
addHook("close_edit", () => $('.topic_block').removeClass("edithead"));
})();
$(document).ready(() => {
$(".alerts").click((event) => {
event.stopPropagation();
var alerts = $(".menu_alerts")[0];
if($(alerts).hasClass("selectedAlert")) return;
if(!conn) loadAlerts(alerts);
alerts.className += " selectedAlert";
document.getElementById("back").className += " alertActive"
addInitHook("end_init", () => {
$(".alerts").click((event) => {
event.stopPropagation();
var alerts = $(".menu_alerts")[0];
if($(alerts).hasClass("selectedAlert")) return;
if(!conn) loadAlerts(alerts);
alerts.className += " selectedAlert";
document.getElementById("back").className += " alertActive"
});
// Move the alerts above the first header
let colSel = $(".colstack_right .colstack_head:first");
let colSelAlt = $(".colstack_right .colstack_item:first");
let colSelAltAlt = $(".colstack_right .coldyn_block:first");
if(colSel.length > 0) $('.alert').insertBefore(colSel);
else if (colSelAlt.length > 0) $('.alert').insertBefore(colSelAlt);
else if (colSelAltAlt.length > 0) $('.alert').insertBefore(colSelAltAlt);
else $('.alert').insertAfter(".rowhead:first");
});
// Move the alerts above the first header
let colSel = $(".colstack_right .colstack_head:first");
let colSelAlt = $(".colstack_right .colstack_item:first");
let colSelAltAlt = $(".colstack_right .coldyn_block:first");
if(colSel.length > 0) {
$('.alert').insertBefore(colSel);
} else if (colSelAlt.length > 0) {
$('.alert').insertBefore(colSelAlt);
} else if (colSelAltAlt.length > 0) {
$('.alert').insertBefore(colSelAltAlt);
} else {
$('.alert').insertAfter(".rowhead:first");
}
});
})();

View File

@ -24,8 +24,37 @@
.menu_stats {
margin-left: 4px;
}
.back_to_site {
/*.back_to_site {
font-size: 18px;
}*/
.above_right {
background-color: rgb(62, 62, 62);
margin-top: -12px;
margin-left: -24px;
margin-right: -24px;
display: flex;
}
.above_right .left_bit {
padding-left: 20px;
margin-top: 16px;
font-size: 18px;
}
.above_right .right_bit {
margin-left: auto;
display: flex;
background-color: rgb(72, 72, 72);
padding-top: 12px;
padding-bottom: 12px;
padding-right: 20px;
padding-left: 20px;
}
.above_right img {
border-radius: 24px;
}
.above_right span {
margin-left: 12px;
margin-top: 5px;
color: rgb(180, 180, 180);
}
.colstack_right {

View File

@ -31,7 +31,8 @@
},
{
"Name":"nox/misc.js",
"Location":"global"
"Location":"global",
"Async":true
}
]
}