diff --git a/main.go b/main.go index cfb70b7..e69bb90 100644 --- a/main.go +++ b/main.go @@ -174,29 +174,6 @@ func main() { // {{{ os.Exit(1) } } // }}} -func scheduleHandler() { // {{{ - // Wait for the approximate minute. - wait := 60000 - time.Now().Sub(time.Now().Truncate(time.Minute)).Milliseconds() - logger.Info("schedule", "wait", wait/1000) - time.Sleep(time.Millisecond * time.Duration(wait)) - tick := time.NewTicker(time.Minute) - for { - for _, event := range ExpiredSchedules() { - notificationManager.Send( - event.UserID, - event.ScheduleUUID, - []byte( - fmt.Sprintf( - "%s\n%s", - event.Time.Format("2006-01-02 15:04"), - event.Description, - ), - ), - ) - } - <-tick.C - } -} // }}} func nodeTree(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{ logger.Info("webserver", "request", "/node/tree") @@ -783,8 +760,20 @@ func scheduleList(w http.ResponseWriter, r *http.Request, sess *session.T) { // var err error w.Header().Add("Access-Control-Allow-Origin", "*") + request := struct { + NodeID int + }{} + body, _ := io.ReadAll(r.Body) + if len(body) > 0 { + err = json.Unmarshal(body, &request) + if err != nil { + responseError(w, err) + return + } + } + var schedules []Schedule - schedules, err = FutureSchedules(sess.UserID) + schedules, err = FutureSchedules(sess.UserID, request.NodeID) if err != nil { responseError(w, err) return diff --git a/node.go b/node.go index f453392..66b5a3e 100644 --- a/node.go +++ b/node.go @@ -326,7 +326,7 @@ func CreateNode(userID, parentID int, name string) (node Node, err error) { // { } // }}} func UpdateNode(userID, nodeID, timeOffset int, content string, cryptoKeyID int, markdown bool) (err error) { // {{{ var timezone string - row := service.Db.Conn.QueryRow(`SELECT timezone FROM public.user WHERE id=$1`, userID) + row := service.Db.Conn.QueryRow(`SELECT timezone FROM _webservice.user WHERE id=$1`, userID) err = row.Scan(&timezone) if err != nil { err = werr.Wrap(err).WithCode("002-000F") diff --git a/schedule.go b/schedule.go index 3520283..823126a 100644 --- a/schedule.go +++ b/schedule.go @@ -28,6 +28,32 @@ type Schedule struct { Acknowledged bool } +func scheduleHandler() { // {{{ + // Wait for the approximate minute. + wait := 60000 - time.Now().Sub(time.Now().Truncate(time.Minute)).Milliseconds() + logger.Info("schedule", "wait", wait/1000) + time.Sleep(time.Millisecond * time.Duration(wait)) + tick := time.NewTicker(time.Minute) + for { + schedules := ExpiredSchedules() + logger.Info("FOO", "schedules", schedules) + for _, event := range schedules { + notificationManager.Send( + event.UserID, + event.ScheduleUUID, + []byte( + fmt.Sprintf( + "%s\n%s", + event.Time.Format("2006-01-02 15:04"), + event.Description, + ), + ), + ) + } + <-tick.C + } +} // }}} + func ScanForSchedules(timezone string, content string) (schedules []Schedule) { // {{{ schedules = []Schedule{} @@ -89,7 +115,7 @@ func RetrieveSchedules(userID int, nodeID int) (schedules []Schedule, err error) s.description, s.acknowledged FROM schedule s - INNER JOIN public.user u ON s.user_id = u.id + INNER JOIN _webservice.user u ON s.user_id = u.id WHERE user_id=$1 AND CASE @@ -120,6 +146,7 @@ func (a Schedule) IsEqual(b Schedule) bool { // {{{ return a.UserID == b.UserID && a.Node.ID == b.Node.ID && a.Time.Equal(b.Time) && + a.RemindMinutes == b.RemindMinutes && a.Description == b.Description } // }}} func (s *Schedule) Insert(queryable Queryable) error { // {{{ @@ -166,9 +193,9 @@ func ExpiredSchedules() (schedules []Schedule) { // {{{ (s.time - MAKE_INTERVAL(mins => s.remind_minutes)) AT TIME ZONE u.timezone AS time, s.description FROM schedule s - INNER JOIN public.user u ON s.user_id = u.id + INNER JOIN _webservice.user u ON s.user_id = u.id WHERE - (time - MAKE_INTERVAL(mins => remind_minutes)) < NOW() AND + (time - MAKE_INTERVAL(mins => remind_minutes)) AT TIME ZONE u.timezone < NOW() AND NOT acknowledged ORDER BY time ASC @@ -189,7 +216,7 @@ func ExpiredSchedules() (schedules []Schedule) { // {{{ } return } // }}} -func FutureSchedules(userID int) (schedules []Schedule, err error) {// {{{ +func FutureSchedules(userID int, nodeID int) (schedules []Schedule, err error) {// {{{ schedules = []Schedule{} res := service.Db.Conn.QueryRow(` @@ -197,16 +224,27 @@ func FutureSchedules(userID int) (schedules []Schedule, err error) {// {{{ SELECT s.id, s.user_id, - to_jsonb(n.*) AS node, + jsonb_build_object( + 'id', n.id, + 'name', n.name, + 'updated', n.updated + ) AS node, s.schedule_uuid, - (time - MAKE_INTERVAL(mins => s.remind_minutes)) AT TIME ZONE u.timezone AS time, + time AT TIME ZONE 'UTC' AS time, s.description, - s.acknowledged + s.acknowledged, + s.remind_minutes AS RemindMinutes FROM schedule s - INNER JOIN public.user u ON s.user_id = u.id + INNER JOIN _webservice.user u ON s.user_id = u.id INNER JOIN node n ON s.node_id = n.id WHERE - s.user_id=$1 AND + s.user_id = $1 AND + ( + CASE + WHEN $2 > 0 THEN n.id = $2 + ELSE true + END + ) AND time >= NOW() AND NOT acknowledged ) @@ -215,6 +253,7 @@ func FutureSchedules(userID int) (schedules []Schedule, err error) {// {{{ FROM schedule_events s `, userID, + nodeID, ) var j []byte err = res.Scan(&j) diff --git a/sql/00021.sql b/sql/00021.sql new file mode 100644 index 0000000..88d7364 --- /dev/null +++ b/sql/00021.sql @@ -0,0 +1 @@ +ALTER TABLE public.node ALTER COLUMN updated TYPE timestamptz USING updated::timestamptz; diff --git a/static/css/main.css b/static/css/main.css index a1d51e5..b4a783f 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -526,7 +526,7 @@ header .menu { grid-area: 1 / 1 / 2 / 2; } /* ============================================================= */ -#file-section { +#schedule-section { grid-area: files; justify-self: center; width: calc(100% - 32px); @@ -536,6 +536,23 @@ header .menu { border-radius: 8px; margin-top: 32px; margin-bottom: 32px; + color: #000; +} +#schedule-section .header { + font-weight: bold; + color: #000; + margin-bottom: 16px; +} +#file-section { + grid-area: schedule; + justify-self: center; + width: calc(100% - 32px); + max-width: 900px; + padding: 32px; + background: #f5f5f5; + border-radius: 8px; + margin-top: 32px; + margin-bottom: 16px; } #file-section .header { font-weight: bold; @@ -634,9 +651,9 @@ header .menu { } .layout-tree { display: grid; - grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree checklist" "tree files" "tree blank"; + grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree checklist" "tree schedule" "tree files" "tree blank"; grid-template-columns: min-content 1fr; - grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* checklist */ min-content /* files */ 1fr; + grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* checklist */ min-content /* schedule */ min-content /* files */ 1fr; /* blank */ color: #fff; min-height: 100%; @@ -669,9 +686,9 @@ header .menu { display: block; } .layout-crumbs { - grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "checklist" "files" "blank"; + grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "checklist" "schedule" "files" "blank"; grid-template-columns: 1fr; - grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* checklist */ min-content /* files */ 1fr; + grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* checklist */ min-content /* schedule */ min-content /* files */ 1fr; /* blank */ } .layout-crumbs #tree { @@ -718,17 +735,17 @@ header .menu { } #app.node { display: grid; - grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree checklist" "tree files" "tree blank"; + grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree checklist" "tree schedule" "tree files" "tree blank"; grid-template-columns: min-content 1fr; - grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* checklist */ min-content /* files */ 1fr; + grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* checklist */ min-content /* schedule */ min-content /* files */ 1fr; /* blank */ color: #fff; min-height: 100%; } #app.node.toggle-tree { - grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "checklist" "files" "blank"; + grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "checklist" "schedule" "files" "blank"; grid-template-columns: 1fr; - grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* checklist */ min-content /* files */ 1fr; + grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* checklist */ min-content /* schedule */ min-content /* files */ 1fr; /* blank */ } #app.node.toggle-tree #tree { @@ -750,11 +767,22 @@ header .menu { #profile-settings .passwords div { white-space: nowrap; } +#schedule-events { + display: grid; + grid-template-columns: repeat(5, min-content); + grid-gap: 4px 12px; + margin: 32px; + color: #000; + white-space: nowrap; +} +#schedule-events .header { + font-weight: bold; +} @media only screen and (max-width: 932px) { #app.node { - grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "checklist" "files" "blank"; + grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "checklist" "schedule" "files" "blank"; grid-template-columns: 1fr; - grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* checklist */ min-content /* files */ 1fr; + grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* checklist */ min-content /* schedule */ min-content /* files */ 1fr; /* blank */ } #app.node #tree { diff --git a/static/js/node.mjs b/static/js/node.mjs index 3608ab7..be03626 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -14,7 +14,6 @@ export class NodeUI extends Component { this.nodeContent = createRef() this.nodeProperties = createRef() this.keys = signal([]) - this.page = signal('node') window.addEventListener('popstate', evt => { if (evt.state && evt.state.hasOwnProperty('nodeID')) @@ -59,7 +58,7 @@ export class NodeUI extends Component { case 'node': if (node.ID == 0) { page = html` -