Add title, priority, tags to cache; add schema migration

pull/26/head
Philipp Heckel 2021-11-28 14:07:29 -05:00
parent 1b8ebab5f3
commit d4330e86ac
3 changed files with 127 additions and 19 deletions

View File

@ -3,32 +3,61 @@ package server
import ( import (
"database/sql" "database/sql"
"errors" "errors"
"fmt"
_ "github.com/mattn/go-sqlite3" // SQLite driver _ "github.com/mattn/go-sqlite3" // SQLite driver
"strings"
"time" "time"
) )
// Messages cache
const ( const (
createTableQuery = ` createMessagesTableQuery = `
BEGIN; BEGIN;
CREATE TABLE IF NOT EXISTS messages ( CREATE TABLE IF NOT EXISTS messages (
id VARCHAR(20) PRIMARY KEY, id VARCHAR(20) PRIMARY KEY,
time INT NOT NULL, time INT NOT NULL,
topic VARCHAR(64) NOT NULL, topic VARCHAR(64) NOT NULL,
message VARCHAR(1024) NOT NULL message VARCHAR(512) NOT NULL,
title VARCHAR(256) NOT NULL,
priority INT NOT NULL,
tags VARCHAR(256) NOT NULL
); );
CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic); CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
COMMIT; COMMIT;
` `
insertMessageQuery = `INSERT INTO messages (id, time, topic, message) VALUES (?, ?, ?, ?)` insertMessageQuery = `INSERT INTO messages (id, time, topic, message, title, priority, tags) VALUES (?, ?, ?, ?, ?, ?, ?)`
pruneMessagesQuery = `DELETE FROM messages WHERE time < ?` pruneMessagesQuery = `DELETE FROM messages WHERE time < ?`
selectMessagesSinceTimeQuery = ` selectMessagesSinceTimeQuery = `
SELECT id, time, message SELECT id, time, message, title, priority, tags
FROM messages FROM messages
WHERE topic = ? AND time >= ? WHERE topic = ? AND time >= ?
ORDER BY time ASC ORDER BY time ASC
` `
selectMessageCountQuery = `SELECT count(*) FROM messages WHERE topic = ?` selectMessagesCountQuery = `SELECT COUNT(*) FROM messages`
selectTopicsQuery = `SELECT topic, MAX(time) FROM messages GROUP BY TOPIC` selectMessageCountForTopicQuery = `SELECT count(*) FROM messages WHERE topic = ?`
selectTopicsQuery = `SELECT topic, MAX(time) FROM messages GROUP BY topic`
)
// Schema management queries
const (
currentSchemaVersion = 1
createSchemaVersionTableQuery = `
CREATE TABLE IF NOT EXISTS schemaVersion (
id INT PRIMARY KEY,
version INT NOT NULL
);
`
insertSchemaVersion = `INSERT INTO schemaVersion VALUES (1, ?)`
selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1`
// 0 -> 1
migrate0To1AlterMessagesTableQuery = `
BEGIN;
ALTER TABLE messages ADD COLUMN title VARCHAR(256) NOT NULL DEFAULT('');
ALTER TABLE messages ADD COLUMN priority INT NOT NULL DEFAULT(0);
ALTER TABLE messages ADD COLUMN tags VARCHAR(256) NOT NULL DEFAULT('');
COMMIT;
`
) )
type sqliteCache struct { type sqliteCache struct {
@ -42,7 +71,7 @@ func newSqliteCache(filename string) (*sqliteCache, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if _, err := db.Exec(createTableQuery); err != nil { if err := setupDB(db); err != nil {
return nil, err return nil, err
} }
return &sqliteCache{ return &sqliteCache{
@ -51,7 +80,7 @@ func newSqliteCache(filename string) (*sqliteCache, error) {
} }
func (c *sqliteCache) AddMessage(m *message) error { func (c *sqliteCache) AddMessage(m *message) error {
_, err := c.db.Exec(insertMessageQuery, m.ID, m.Time, m.Topic, m.Message) _, err := c.db.Exec(insertMessageQuery, m.ID, m.Time, m.Topic, m.Message, m.Title, m.Priority, strings.Join(m.Tags, ","))
return err return err
} }
@ -64,19 +93,27 @@ func (c *sqliteCache) Messages(topic string, since sinceTime) ([]*message, error
messages := make([]*message, 0) messages := make([]*message, 0)
for rows.Next() { for rows.Next() {
var timestamp int64 var timestamp int64
var id, msg string var priority int
if err := rows.Scan(&id, &timestamp, &msg); err != nil { var id, msg, title, tagsStr string
if err := rows.Scan(&id, &timestamp, &msg, &title, &priority, &tagsStr); err != nil {
return nil, err return nil, err
} }
if msg == "" { if msg == "" {
msg = " " // Hack: never return empty messages; this should not happen msg = " " // Hack: never return empty messages; this should not happen
} }
var tags []string
if tagsStr != "" {
tags = strings.Split(tagsStr, ",")
}
messages = append(messages, &message{ messages = append(messages, &message{
ID: id, ID: id,
Time: timestamp, Time: timestamp,
Event: messageEvent, Event: messageEvent,
Topic: topic, Topic: topic,
Message: msg, Message: msg,
Title: title,
Priority: priority,
Tags: tags,
}) })
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
@ -86,7 +123,7 @@ func (c *sqliteCache) Messages(topic string, since sinceTime) ([]*message, error
} }
func (c *sqliteCache) MessageCount(topic string) (int, error) { func (c *sqliteCache) MessageCount(topic string) (int, error) {
rows, err := c.db.Query(selectMessageCountQuery, topic) rows, err := c.db.Query(selectMessageCountForTopicQuery, topic)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -124,7 +161,63 @@ func (s *sqliteCache) Topics() (map[string]*topic, error) {
return topics, nil return topics, nil
} }
func (c *sqliteCache) Prune(keep time.Duration) error { func (s *sqliteCache) Prune(keep time.Duration) error {
_, err := c.db.Exec(pruneMessagesQuery, time.Now().Add(-1*keep).Unix()) _, err := s.db.Exec(pruneMessagesQuery, time.Now().Add(-1*keep).Unix())
return err return err
} }
func setupDB(db *sql.DB) error {
// If 'messages' table does not exist, this must be a new database
rowsMC, err := db.Query(selectMessagesCountQuery)
if err != nil {
return setupNewDB(db)
}
defer rowsMC.Close()
// If 'messages' table exists, check 'schemaVersion' table
schemaVersion := 0
rowsSV, err := db.Query(selectSchemaVersionQuery)
if err == nil {
defer rowsSV.Close()
if !rowsSV.Next() {
return errors.New("cannot determine schema version: cache file may be corrupt")
}
if err := rowsSV.Scan(&schemaVersion); err != nil {
return err
}
}
// Do migrations
if schemaVersion == currentSchemaVersion {
return nil
} else if schemaVersion == 0 {
return migrateFrom0To1(db)
}
return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
}
func setupNewDB(db *sql.DB) error {
if _, err := db.Exec(createMessagesTableQuery); err != nil {
return err
}
if _, err := db.Exec(createSchemaVersionTableQuery); err != nil {
return err
}
if _, err := db.Exec(insertSchemaVersion, currentSchemaVersion); err != nil {
return err
}
return nil
}
func migrateFrom0To1(db *sql.DB) error {
if _, err := db.Exec(migrate0To1AlterMessagesTableQuery); err != nil {
return err
}
if _, err := db.Exec(createSchemaVersionTableQuery); err != nil {
return err
}
if _, err := db.Exec(insertSchemaVersion, 1); err != nil {
return err
}
return nil
}

View File

@ -382,6 +382,11 @@ li {
font-size: 0.9em; font-size: 0.9em;
} }
#detail .detailTitle {
font-weight: bold;
font-size: 1.1em;
}
#detail .detailMessage { #detail .detailMessage {
margin-bottom: 20px; margin-bottom: 20px;
font-size: 1.1em; font-size: 1.1em;

View File

@ -86,10 +86,14 @@ const subscribeInternal = (topic, persist, delaySec) => {
} }
if (Notification.permission === "granted") { if (Notification.permission === "granted") {
notifySound.play(); notifySound.play();
new Notification(`${location.host}/${topic}`, { const title = (event.title) ? event.title : `${location.host}/${topic}`;
const notification = new Notification(title, {
body: event.message, body: event.message,
icon: '/static/img/favicon.png' icon: '/static/img/favicon.png'
}); });
notification.onclick = (e) => {
showDetail(event.topic);
};
} }
}; };
topics[topic] = { topics[topic] = {
@ -149,6 +153,7 @@ const rerenderDetailView = () => {
} }
topics[currentTopic]['messages'].forEach(m => { topics[currentTopic]['messages'].forEach(m => {
let dateDiv = document.createElement('div'); let dateDiv = document.createElement('div');
let titleDiv = document.createElement('div');
let messageDiv = document.createElement('div'); let messageDiv = document.createElement('div');
let eventDiv = document.createElement('div'); let eventDiv = document.createElement('div');
dateDiv.classList.add('detailDate'); dateDiv.classList.add('detailDate');
@ -156,6 +161,11 @@ const rerenderDetailView = () => {
messageDiv.classList.add('detailMessage'); messageDiv.classList.add('detailMessage');
messageDiv.innerText = m.message; messageDiv.innerText = m.message;
eventDiv.appendChild(dateDiv); eventDiv.appendChild(dateDiv);
if (m.title) {
titleDiv.classList.add('detailTitle');
titleDiv.innerText = m.title;
eventDiv.appendChild(titleDiv)
}
eventDiv.appendChild(messageDiv); eventDiv.appendChild(messageDiv);
detailEventsList.appendChild(eventDiv); detailEventsList.appendChild(eventDiv);
}) })