diff --git a/.gitignore b/.gitignore index 7d5585e..1202234 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ notes2 -untracked diff --git a/config.go b/config.go index 71d5f5d..0ba4ea2 100644 --- a/config.go +++ b/config.go @@ -3,6 +3,7 @@ package main import ( // Standard "encoding/json" + "fmt" "os" ) @@ -26,8 +27,9 @@ type Config struct { } } -func readConfig(fname string) (err error) { +func readConfig() (err error) { var configData []byte + fname := fmt.Sprintf("%s/.config/notes2.json", os.Getenv("HOME")) configData, err = os.ReadFile(fname) if err != nil { return diff --git a/html_template/pkg.go b/html_template/pkg.go index a478789..4140f89 100644 --- a/html_template/pkg.go +++ b/html_template/pkg.go @@ -11,7 +11,6 @@ import ( "net/http" "os" "regexp" - "sync" ) type Engine struct { @@ -23,10 +22,6 @@ type Engine struct { DevMode bool } -var ( - templateLock sync.Mutex -) - func NewEngine(viewFS, staticFS fs.FS, devmode bool) (e Engine, err error) { // {{{ e.parsedTemplates = make(map[string]*template.Template) e.viewFS = viewFS @@ -67,12 +62,12 @@ func (e *Engine) getComponentFilenames() (files []string, err error) { // {{{ } // }}} func (e *Engine) ReloadTemplates() { // {{{ - templateLock.Lock() e.parsedTemplates = make(map[string]*template.Template) - templateLock.Unlock() } // }}} func (e *Engine) StaticResource(w http.ResponseWriter, r *http.Request) { // {{{ + var err error + // URLs with pattern /(css|images)/v1.0.0/foobar are stripped of the version. // To get rid of problems with cached content in browser on a new version release, // while also not disabling cache altogether. @@ -88,7 +83,11 @@ func (e *Engine) StaticResource(w http.ResponseWriter, r *http.Request) { // {{{ r.URL.Path = fmt.Sprintf("/%s/%s", comp[1], comp[2]) if e.DevMode { - e.staticLocalFS.ServeHTTP(w, r) + p := fmt.Sprintf("static/%s/%s", comp[1], comp[2]) + _, err = os.Stat(p) + if err == nil { + e.staticLocalFS.ServeHTTP(w, r) + } return } } @@ -126,9 +125,7 @@ func (e *Engine) getPage(layout, page string) (tmpl *template.Template, err erro return } - templateLock.Lock() e.parsedTemplates[page] = tmpl - templateLock.Unlock() return } // }}} func (e *Engine) Render(p Page, w http.ResponseWriter, r *http.Request) (err error) { // {{{ diff --git a/main.go b/main.go index 8edd01c..d25f72f 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ import ( "text/template" ) -const VERSION = "v5" +const VERSION = "v2" const CONTEXT_USER = 1 const SYNC_PAGINATION = 200 @@ -75,7 +75,7 @@ func initLog() { // {{{ } // }}} func main() { // {{{ initLog() - err := readConfig(FlagConfig) + err := readConfig() if err != nil { Log.Error("config", "error", err) os.Exit(1) @@ -129,6 +129,7 @@ func main() { // {{{ } http.HandleFunc("/", rootHandler) + http.HandleFunc("/notes2", pageNotes2) http.HandleFunc("/login", pageLogin) http.HandleFunc("/sync", pageSync) http.HandleFunc("/offline", pageOffline) @@ -188,13 +189,7 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { // {{{ // All URLs not specifically handled are routed to this function. // Everything going here should be a static resource. if r.URL.Path == "/" { - page := NewPage("notes2") - - err := Webengine.Render(page, w, r) - if err != nil { - w.Write([]byte(err.Error())) - return - } + http.Redirect(w, r, "/notes2", http.StatusSeeOther) return } @@ -250,6 +245,15 @@ func pageLogin(w http.ResponseWriter, r *http.Request) { // {{{ return } } // }}} +func pageNotes2(w http.ResponseWriter, r *http.Request) { // {{{ + page := NewPage("notes2") + + err := Webengine.Render(page, w, r) + if err != nil { + w.Write([]byte(err.Error())) + return + } +} // }}} func pageSync(w http.ResponseWriter, r *http.Request) { // {{{ page := NewPage("sync") @@ -276,9 +280,9 @@ func actionSyncFromServer(w http.ResponseWriter, r *http.Request) { // {{{ } /* - Log.Debug("/sync/from_server", "num_nodes", len(nodes), "maxSeq", maxSeq) - foo, _ := json.Marshal(nodes) - os.WriteFile(fmt.Sprintf("/tmp/nodes-%d.json", offset), foo, 0644) + Log.Debug("/sync/from_server", "num_nodes", len(nodes), "maxSeq", maxSeq) + foo, _ := json.Marshal(nodes) + os.WriteFile(fmt.Sprintf("/tmp/nodes-%d.json", offset), foo, 0644) */ j, _ := json.Marshal(struct { diff --git a/sql/00001.sql b/sql/00001.sql index 7eb8273..08d5266 100644 --- a/sql/00001.sql +++ b/sql/00001.sql @@ -1,218 +1,48 @@ --- --- Name: pg_trgm; Type: EXTENSION; Schema: -; Owner: - --- +CREATE EXTENSION IF NOT EXISTS pg_trgm; +CREATE EXTENSION IF NOT EXISTS pgcrypto; -CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public; +CREATE SEQUENCE node_updates; - --- --- Name: EXTENSION pg_trgm; Type: COMMENT; Schema: -; Owner: --- - -COMMENT ON EXTENSION pg_trgm IS 'text similarity measurement and index searching based on trigrams'; - - --- --- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public; - - --- --- Name: EXTENSION pgcrypto; Type: COMMENT; Schema: -; Owner: --- - -COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions'; - - --- --- Name: json_ancestor_array; Type: TYPE; Schema: public; Owner: postgres --- - -CREATE TYPE public.json_ancestor_array AS ( - "Ancestors" character varying[] +CREATE TABLE public."user" ( + id serial4 NOT NULL, + username varchar NOT NULL, + "name" varchar NOT NULL, + "password" varchar NOT NULL, + last_login timestamp DEFAULT now() NOT NULL, + timezone varchar DEFAULT 'UTC'::character varying NOT NULL, + CONSTRAINT user_pk PRIMARY KEY (id) ); --- --- Name: add_nodes(integer, character varying, jsonb); Type: PROCEDURE; Schema: public; Owner: postgres --- +CREATE TABLE public.node ( + id serial4 NOT NULL, + user_id int4 NOT NULL, + "uuid" bpchar(36) DEFAULT gen_random_uuid() NOT NULL, + parent_uuid bpchar(36) NULL, -CREATE PROCEDURE public.add_nodes(IN p_user_id integer, IN p_client_uuid character varying, IN p_nodes jsonb) - LANGUAGE plpgsql - AS $$ + created timestamptz DEFAULT NOW() NOT NULL, + updated timestamptz DEFAULT NOW() NOT NULL, + deleted timestamptz NULL, -DECLARE - node_data jsonb; - node_updated timestamptz; - db_updated timestamptz; - db_uuid bpchar; - db_client bpchar; - db_client_seq int; - node_uuid bpchar; - parent_uuid_nullable bpchar; + created_seq bigint NOT NULL DEFAULT nextval('node_updates'), + updated_seq bigint NOT NULL DEFAULT nextval('node_updates'), + deleted_seq bigint NULL, -BEGIN - RAISE NOTICE '--------------------------'; - FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes) - LOOP - node_uuid = (node_data->>'UUID')::bpchar; - node_updated = (node_data->>'Updated')::timestamptz; + "name" varchar(256) DEFAULT ''::character varying NOT NULL, + "content" text DEFAULT ''::text NOT NULL, + content_encrypted text DEFAULT ''::text NOT NULL, + markdown bool DEFAULT false NOT NULL, - IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' THEN - parent_uuid_nullable = NULL; - ELSE - parent_uuid_nullable = node_data->>'ParentUUID'; - END IF; + CONSTRAINT name_length CHECK ((length(TRIM(BOTH FROM name)) > 0)), + CONSTRAINT node_pk PRIMARY KEY (id), + CONSTRAINT user_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE RESTRICT ON UPDATE RESTRICT +); +CREATE UNIQUE INDEX node_uuid_idx ON public.node USING btree (uuid); +CREATE INDEX node_search_index ON public.node USING gin (name gin_trgm_ops, content gin_trgm_ops); - /* Retrieve the current modified timestamp for this node from the database. */ - SELECT - uuid, updated, client, client_sequence - INTO - db_uuid, db_updated, db_client, db_client_seq - FROM public."node" - WHERE - user_id = p_user_id AND - uuid = node_uuid; - - /* Is the node not in database? It needs to be created. */ - IF db_uuid IS NULL THEN - RAISE NOTICE '01 New node %', node_uuid; - INSERT INTO public."node" ( - user_id, "uuid", parent_uuid, created, updated, - "name", "content", markdown, "content_encrypted", - client, client_sequence - ) - VALUES( - p_user_id, - node_uuid, - parent_uuid_nullable, - (node_data->>'Created')::timestamptz, - (node_data->>'Updated')::timestamptz, - (node_data->>'Name')::varchar, - (node_data->>'Content')::text, - (node_data->>'Markdown')::bool, - '', /* content_encrypted */ - p_client_uuid, - (node_data->>'ClientSequence')::int - ); - CONTINUE; - END IF; - - - /* The client could send a specific node again if it didn't receive the OK from this procedure before. */ - IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN - RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq; - CONTINUE; - END IF; - - /* Determine if the incoming node data is to go into history or replace the current node. */ - IF db_updated > node_updated THEN - RAISE NOTICE '02 DB newer, % > % (%))', db_updated, node_updated, node_uuid; - /* Incoming node is going straight to history since it is older than the current node. */ - INSERT INTO node_history( - user_id, "uuid", parents, created, updated, - "name", "content", markdown, "content_encrypted", - client, client_sequence - ) - VALUES( - p_user_id, - node_uuid, - (jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors", - (node_data->>'Created')::timestamptz, - (node_data->>'Updated')::timestamptz, - (node_data->>'Name')::varchar, - (node_data->>'Content')::text, - (node_data->>'Markdown')::bool, - '', /* content_encrypted */ - p_client_uuid, - (node_data->>'ClientSequence')::int - ) - ON CONFLICT (client, client_sequence) - DO NOTHING; - ELSE - RAISE NOTICE '03 Client newer, % > % (%, %)', node_updated, db_updated, node_uuid, (node_data->>'ClientSequence'); - /* Incoming node is newer and will replace the current node. - * - * The current node is copied to the node_history table and then modified in place - * with the incoming data. */ - INSERT INTO node_history( - user_id, "uuid", parents, - created, updated, "name", "content", markdown, "content_encrypted", - client, client_sequence - ) - SELECT - user_id, - "uuid", - ( - WITH RECURSIVE nodes AS ( - SELECT - uuid, - COALESCE(parent_uuid, '') AS parent_uuid, - name, - 0 AS depth - FROM node - WHERE - uuid = node_uuid - - UNION - - SELECT - n.uuid, - COALESCE(n.parent_uuid, '') AS parent_uuid, - n.name, - nr.depth+1 AS depth - FROM node n - INNER JOIN nodes nr ON n.uuid = nr.parent_uuid - ) - SELECT ARRAY ( - SELECT name - FROM nodes - ORDER BY depth DESC - OFFSET 1 /* discard itself */ - ) - ), - created, - updated, - name, - content, - markdown, - content_encrypted, - client, - client_sequence - FROM public."node" - WHERE - user_id = p_user_id AND - uuid = node_uuid - ON CONFLICT (client, client_sequence) - DO NOTHING; - - /* Current node in database is updated with incoming data. */ - UPDATE public."node" - SET - updated = (node_data->>'Updated')::timestamptz, - updated_seq = nextval('node_updates'), - name = (node_data->>'Name')::varchar, - content = (node_data->>'Content')::text, - markdown = (node_data->>'Markdown')::bool, - client = p_client_uuid, - client_sequence = (node_data->>'ClientSequence')::int - WHERE - user_id = p_user_id AND - uuid = node_uuid; - END IF; - - END LOOP; -END -$$; - --- --- Name: node_update_timestamp(); Type: FUNCTION; Schema: public; Owner: postgres --- - -CREATE FUNCTION public.node_update_timestamp() RETURNS trigger - LANGUAGE plpgsql - AS $$ +CREATE OR REPLACE FUNCTION node_update_timestamp() +RETURNS TRIGGER +LANGUAGE PLPGSQL +AS $$ BEGIN IF NEW.updated = OLD.updated THEN UPDATE node @@ -226,335 +56,6 @@ BEGIN END; $$; - --- --- Name: password_hash(character, bytea); Type: FUNCTION; Schema: public; Owner: postgres --- - -CREATE FUNCTION public.password_hash(salt_hex character, pass bytea) RETURNS character - LANGUAGE plpgsql - AS $$ -BEGIN - RETURN ( - SELECT - salt_hex || - encode( - sha256( - decode(salt_hex, 'hex') || /* salt in binary */ - pass /* password */ - ), - 'hex' - ) - ); -END; -$$; - - --- --- Name: client; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.client ( - id integer NOT NULL, - user_id integer NOT NULL, - client_uuid character(36) DEFAULT ''::bpchar NOT NULL, - created timestamp with time zone DEFAULT now() NOT NULL, - description character varying DEFAULT ''::character varying NOT NULL -); - - --- --- Name: client_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres --- - -CREATE SEQUENCE public.client_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: client_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres --- - -ALTER SEQUENCE public.client_id_seq OWNED BY public.client.id; - - --- --- Name: node_updates; Type: SEQUENCE; Schema: public; Owner: postgres --- - -CREATE SEQUENCE public.node_updates - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: node; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.node ( - id integer NOT NULL, - user_id integer NOT NULL, - uuid character(36) DEFAULT gen_random_uuid() NOT NULL, - parent_uuid character(36), - created timestamp with time zone DEFAULT now() NOT NULL, - updated timestamp with time zone DEFAULT now() NOT NULL, - deleted timestamp with time zone, - created_seq bigint DEFAULT nextval('public.node_updates'::regclass) NOT NULL, - updated_seq bigint DEFAULT nextval('public.node_updates'::regclass) NOT NULL, - deleted_seq bigint, - name character varying(256) DEFAULT ''::character varying NOT NULL, - content text DEFAULT ''::text NOT NULL, - content_encrypted text DEFAULT ''::text NOT NULL, - markdown boolean DEFAULT false NOT NULL, - history boolean DEFAULT false NOT NULL, - client character(36) DEFAULT ''::bpchar NOT NULL, - client_sequence integer, - CONSTRAINT name_length CHECK ((length(TRIM(BOTH FROM name)) > 0)) -); - - --- --- Name: node_history; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.node_history ( - id integer NOT NULL, - user_id integer NOT NULL, - uuid character(36) NOT NULL, - parents character varying[], - created timestamp with time zone NOT NULL, - updated timestamp with time zone NOT NULL, - name character varying(256) NOT NULL, - content text NOT NULL, - content_encrypted text NOT NULL, - markdown boolean DEFAULT false NOT NULL, - client character(36) DEFAULT ''::bpchar NOT NULL, - client_sequence integer -); - - --- --- Name: node_history_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres --- - -CREATE SEQUENCE public.node_history_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: node_history_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres --- - -ALTER SEQUENCE public.node_history_id_seq OWNED BY public.node_history.id; - - --- --- Name: node_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres --- - -CREATE SEQUENCE public.node_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: node_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres --- - -ALTER SEQUENCE public.node_id_seq OWNED BY public.node.id; - - --- --- Name: test_data; Type: SEQUENCE; Schema: public; Owner: postgres --- - -CREATE SEQUENCE public.test_data - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: user; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public."user" ( - id integer NOT NULL, - username character varying NOT NULL, - name character varying NOT NULL, - password character varying NOT NULL, - last_login timestamp without time zone DEFAULT now() NOT NULL, - timezone character varying DEFAULT 'UTC'::character varying NOT NULL -); - - --- --- Name: user_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres --- - -CREATE SEQUENCE public.user_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: user_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres --- - -ALTER SEQUENCE public.user_id_seq OWNED BY public."user".id; - - --- --- Name: client id; Type: DEFAULT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.client ALTER COLUMN id SET DEFAULT nextval('public.client_id_seq'::regclass); - - --- --- Name: node id; Type: DEFAULT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.node ALTER COLUMN id SET DEFAULT nextval('public.node_id_seq'::regclass); - - --- --- Name: node_history id; Type: DEFAULT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.node_history ALTER COLUMN id SET DEFAULT nextval('public.node_history_id_seq'::regclass); - - --- --- Name: user id; Type: DEFAULT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public."user" ALTER COLUMN id SET DEFAULT nextval('public.user_id_seq'::regclass); - - --- --- Name: client client_pk; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.client - ADD CONSTRAINT client_pk PRIMARY KEY (id); - - --- --- Name: node_history node_history_pk; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.node_history - ADD CONSTRAINT node_history_pk PRIMARY KEY (id); - - --- --- Name: node node_pk; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.node - ADD CONSTRAINT node_pk PRIMARY KEY (id); - - --- --- Name: user user_pk; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public."user" - ADD CONSTRAINT user_pk PRIMARY KEY (id); - - --- --- Name: client_uuid_idx; Type: INDEX; Schema: public; Owner: postgres --- - -CREATE UNIQUE INDEX client_uuid_idx ON public.client USING btree (client_uuid); - - --- --- Name: node_history_client_idx; Type: INDEX; Schema: public; Owner: postgres --- - -CREATE UNIQUE INDEX node_history_client_idx ON public.node_history USING btree (client, client_sequence); - - --- --- Name: node_history_idx; Type: INDEX; Schema: public; Owner: postgres --- - -CREATE INDEX node_history_idx ON public.node USING btree (history); - - --- --- Name: node_history_uuid_idx; Type: INDEX; Schema: public; Owner: postgres --- - -CREATE INDEX node_history_uuid_idx ON public.node USING btree (uuid); - - --- --- Name: node_search_index; Type: INDEX; Schema: public; Owner: postgres --- - -CREATE INDEX node_search_index ON public.node USING gin (name public.gin_trgm_ops, content public.gin_trgm_ops); - - --- --- Name: node_uuid_idx; Type: INDEX; Schema: public; Owner: postgres --- - -CREATE UNIQUE INDEX node_uuid_idx ON public.node USING btree (uuid); - - --- --- Name: node node_update; Type: TRIGGER; Schema: public; Owner: postgres --- - -CREATE TRIGGER node_update AFTER UPDATE ON public.node FOR EACH ROW EXECUTE FUNCTION public.node_update_timestamp(); - - --- --- Name: node_history node_history_user_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.node_history - ADD CONSTRAINT node_history_user_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON UPDATE RESTRICT ON DELETE RESTRICT; - - --- --- Name: node node_node_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.node - ADD CONSTRAINT node_node_fk FOREIGN KEY (parent_uuid) REFERENCES public.node(uuid) ON UPDATE SET NULL ON DELETE SET NULL; - - --- --- Name: node user_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.node - ADD CONSTRAINT user_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON UPDATE RESTRICT ON DELETE RESTRICT; +CREATE OR REPLACE TRIGGER node_update AFTER UPDATE ON node +FOR EACH ROW +EXECUTE PROCEDURE node_update_timestamp(); diff --git a/sql/00002.sql b/sql/00002.sql new file mode 100644 index 0000000..9d7bd8a --- /dev/null +++ b/sql/00002.sql @@ -0,0 +1,19 @@ +CREATE FUNCTION public.password_hash(salt_hex char(32), pass bytea) +RETURNS char(96) +LANGUAGE plpgsql +AS +$$ +BEGIN + RETURN ( + SELECT + salt_hex || + encode( + sha256( + decode(salt_hex, 'hex') || /* salt in binary */ + pass /* password */ + ), + 'hex' + ) + ); +END; +$$; diff --git a/sql/00003.sql b/sql/00003.sql new file mode 100644 index 0000000..0fb2a51 --- /dev/null +++ b/sql/00003.sql @@ -0,0 +1 @@ +ALTER TABLE public.node ADD CONSTRAINT node_node_fk FOREIGN KEY (parent_uuid) REFERENCES public.node("uuid") ON DELETE SET NULL ON UPDATE SET NULL; diff --git a/sql/00004.sql b/sql/00004.sql new file mode 100644 index 0000000..7ac464d --- /dev/null +++ b/sql/00004.sql @@ -0,0 +1,2 @@ +ALTER TABLE public.node ADD COLUMN history bool NOT NULL DEFAULT false; +CREATE INDEX node_history_idx ON public.node (history); diff --git a/sql/00005.sql b/sql/00005.sql new file mode 100644 index 0000000..a366070 --- /dev/null +++ b/sql/00005.sql @@ -0,0 +1 @@ +ALTER TABLE public.node ADD COLUMN client bpchar(36) NOT NULL DEFAULT ''; diff --git a/sql/00006.sql b/sql/00006.sql new file mode 100644 index 0000000..453b260 --- /dev/null +++ b/sql/00006.sql @@ -0,0 +1 @@ +DROP INDEX public.node_uuid_idx; diff --git a/sql/00007.sql b/sql/00007.sql new file mode 100644 index 0000000..40ce48e --- /dev/null +++ b/sql/00007.sql @@ -0,0 +1,162 @@ +CREATE TYPE json_ancestor_array as ("Ancestors" varchar[]); + + +CREATE OR REPLACE PROCEDURE add_nodes(p_user_id int4, p_client_uuid varchar, p_nodes jsonb) +LANGUAGE PLPGSQL AS $$ + +DECLARE + node_data jsonb; + node_updated timestamptz; + db_updated timestamptz; + db_uuid bpchar; + db_client bpchar; + db_client_seq int; + node_uuid bpchar; + +BEGIN + RAISE NOTICE '--------------------------'; + FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes) + LOOP + node_uuid = (node_data->>'UUID')::bpchar; + node_updated = (node_data->>'Updated')::timestamptz; + + /* Retrieve the current modified timestamp for this node from the database. */ + SELECT + uuid, updated, client, client_sequence + INTO + db_uuid, db_updated, db_client, db_client_seq + FROM public."node" + WHERE + user_id = p_user_id AND + uuid = node_uuid; + + /* Is the node not in database? It needs to be created. */ + IF db_uuid IS NULL THEN + RAISE NOTICE '01 New node %', node_uuid; + INSERT INTO public."node" ( + user_id, "uuid", parent_uuid, created, updated, + "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + VALUES( + p_user_id, + node_uuid, + (node_data->>'ParentUUID')::bpchar, + (node_data->>'Created')::timestamptz, + (node_data->>'Updated')::timestamptz, + (node_data->>'Name')::varchar, + (node_data->>'Content')::text, + (node_data->>'Markdown')::bool, + '', /* content_encrypted */ + p_client_uuid, + (node_data->>'ClientSequence')::int + ); + CONTINUE; + END IF; + + + /* The client could send a specific node again if it didn't receive the OK from this procedure before. */ + IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN + RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq; + CONTINUE; + END IF; + + /* Determine if the incoming node data is to go into history or replace the current node. */ + IF db_updated > node_updated THEN + RAISE NOTICE '02 DB newer, % > % (%))', db_updated, node_updated, node_uuid; + /* Incoming node is going straight to history since it is older than the current node. */ + INSERT INTO node_history( + user_id, "uuid", parents, created, updated, + "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + VALUES( + p_user_id, + node_uuid, + (jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors", + (node_data->>'Created')::timestamptz, + (node_data->>'Updated')::timestamptz, + (node_data->>'Name')::varchar, + (node_data->>'Content')::text, + (node_data->>'Markdown')::bool, + '', /* content_encrypted */ + p_client_uuid, + (node_data->>'ClientSequence')::int + ) + ON CONFLICT (client, client_sequence) + DO NOTHING; + ELSE + RAISE NOTICE '03 Client newer, % > % (%, %)', node_updated, db_updated, node_uuid, (node_data->>'ClientSequence'); + /* Incoming node is newer and will replace the current node. + * + * The current node is copied to the node_history table and then modified in place + * with the incoming data. */ + INSERT INTO node_history( + user_id, "uuid", parents, + created, updated, "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + SELECT + user_id, + "uuid", + ( + WITH RECURSIVE nodes AS ( + SELECT + uuid, + COALESCE(parent_uuid, '') AS parent_uuid, + name, + 0 AS depth + FROM node + WHERE + uuid = node_uuid + + UNION + + SELECT + n.uuid, + COALESCE(n.parent_uuid, '') AS parent_uuid, + n.name, + nr.depth+1 AS depth + FROM node n + INNER JOIN nodes nr ON n.uuid = nr.parent_uuid + ) + SELECT ARRAY ( + SELECT name + FROM nodes + ORDER BY depth DESC + OFFSET 1 /* discard itself */ + ) + ), + created, + updated, + name, + content, + markdown, + content_encrypted, + client, + client_sequence + FROM public."node" + WHERE + user_id = p_user_id AND + uuid = node_uuid + ON CONFLICT (client, client_sequence) + DO NOTHING; + + /* Current node in database is updated with incoming data. */ + UPDATE public."node" + SET + updated = (node_data->>'Updated')::timestamptz, + updated_seq = nextval('node_updates'), + name = (node_data->>'Name')::varchar, + content = (node_data->>'Content')::text, + markdown = (node_data->>'Markdown')::bool, + client = p_client_uuid, + client_sequence = (node_data->>'ClientSequence')::int + WHERE + user_id = p_user_id AND + uuid = node_uuid; + END IF; + + END LOOP; +END +$$; diff --git a/sql/00008.sql b/sql/00008.sql new file mode 100644 index 0000000..a91d54c --- /dev/null +++ b/sql/00008.sql @@ -0,0 +1,2 @@ +ALTER TABLE node ADD COLUMN Client_sequence int NULL; +ALTER TABLE node_history ADD COLUMN Client_sequence int NULL; diff --git a/sql/00009.sql b/sql/00009.sql new file mode 100644 index 0000000..332af3a --- /dev/null +++ b/sql/00009.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX node_history_client_idx ON public.node_history (client,client_sequence); diff --git a/sql/00010.sql b/sql/00010.sql new file mode 100644 index 0000000..c0f14ee --- /dev/null +++ b/sql/00010.sql @@ -0,0 +1,10 @@ +CREATE TABLE public.client ( + id serial NOT NULL, + user_id int4 NOT NULL, + client_uuid bpchar(36) DEFAULT '' NOT NULL, + created timestamptz DEFAULT NOW() NOT NULL, + description varchar DEFAULT '' NOT NULL, + CONSTRAINT client_pk PRIMARY KEY (id) +); + +CREATE UNIQUE INDEX client_uuid_idx ON public.client (client_uuid); diff --git a/sql/00011.sql b/sql/00011.sql new file mode 100644 index 0000000..5b67839 --- /dev/null +++ b/sql/00011.sql @@ -0,0 +1,166 @@ +CREATE OR REPLACE PROCEDURE add_nodes(p_user_id int4, p_client_uuid varchar, p_nodes jsonb) +LANGUAGE PLPGSQL AS $$ + +DECLARE + node_data jsonb; + node_updated timestamptz; + db_updated timestamptz; + db_uuid bpchar; + db_client bpchar; + db_client_seq int; + node_uuid bpchar; + parent_uuid bpchar; + +BEGIN + RAISE NOTICE '--------------------------'; + FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes) + LOOP + node_uuid = (node_data->>'UUID')::bpchar; + node_updated = (node_data->>'Updated')::timestamptz; + + IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' THEN + parent_uuid = NULL; + ELSE + parent_uuid = node_data->>'ParentUUID'; + END IF; + + /* Retrieve the current modified timestamp for this node from the database. */ + SELECT + uuid, updated, client, client_sequence + INTO + db_uuid, db_updated, db_client, db_client_seq + FROM public."node" + WHERE + user_id = p_user_id AND + uuid = node_uuid; + + /* Is the node not in database? It needs to be created. */ + IF db_uuid IS NULL THEN + RAISE NOTICE '01 New node %', node_uuid; + INSERT INTO public."node" ( + user_id, "uuid", parent_uuid, created, updated, + "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + VALUES( + p_user_id, + node_uuid, + parent_uuid, + (node_data->>'Created')::timestamptz, + (node_data->>'Updated')::timestamptz, + (node_data->>'Name')::varchar, + (node_data->>'Content')::text, + (node_data->>'Markdown')::bool, + '', /* content_encrypted */ + p_client_uuid, + (node_data->>'ClientSequence')::int + ); + CONTINUE; + END IF; + + + /* The client could send a specific node again if it didn't receive the OK from this procedure before. */ + IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN + RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq; + CONTINUE; + END IF; + + /* Determine if the incoming node data is to go into history or replace the current node. */ + IF db_updated > node_updated THEN + RAISE NOTICE '02 DB newer, % > % (%))', db_updated, node_updated, node_uuid; + /* Incoming node is going straight to history since it is older than the current node. */ + INSERT INTO node_history( + user_id, "uuid", parents, created, updated, + "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + VALUES( + p_user_id, + node_uuid, + (jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors", + (node_data->>'Created')::timestamptz, + (node_data->>'Updated')::timestamptz, + (node_data->>'Name')::varchar, + (node_data->>'Content')::text, + (node_data->>'Markdown')::bool, + '', /* content_encrypted */ + p_client_uuid, + (node_data->>'ClientSequence')::int + ) + ON CONFLICT (client, client_sequence) + DO NOTHING; + ELSE + RAISE NOTICE '03 Client newer, % > % (%, %)', node_updated, db_updated, node_uuid, (node_data->>'ClientSequence'); + /* Incoming node is newer and will replace the current node. + * + * The current node is copied to the node_history table and then modified in place + * with the incoming data. */ + INSERT INTO node_history( + user_id, "uuid", parents, + created, updated, "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + SELECT + user_id, + "uuid", + ( + WITH RECURSIVE nodes AS ( + SELECT + uuid, + COALESCE(parent_uuid, '') AS parent_uuid, + name, + 0 AS depth + FROM node + WHERE + uuid = node_uuid + + UNION + + SELECT + n.uuid, + COALESCE(n.parent_uuid, '') AS parent_uuid, + n.name, + nr.depth+1 AS depth + FROM node n + INNER JOIN nodes nr ON n.uuid = nr.parent_uuid + ) + SELECT ARRAY ( + SELECT name + FROM nodes + ORDER BY depth DESC + OFFSET 1 /* discard itself */ + ) + ), + created, + updated, + name, + content, + markdown, + content_encrypted, + client, + client_sequence + FROM public."node" + WHERE + user_id = p_user_id AND + uuid = node_uuid + ON CONFLICT (client, client_sequence) + DO NOTHING; + + /* Current node in database is updated with incoming data. */ + UPDATE public."node" + SET + updated = (node_data->>'Updated')::timestamptz, + updated_seq = nextval('node_updates'), + name = (node_data->>'Name')::varchar, + content = (node_data->>'Content')::text, + markdown = (node_data->>'Markdown')::bool, + client = p_client_uuid, + client_sequence = (node_data->>'ClientSequence')::int + WHERE + user_id = p_user_id AND + uuid = node_uuid; + END IF; + + END LOOP; +END +$$; diff --git a/sql/00012.sql b/sql/00012.sql new file mode 100644 index 0000000..e62f011 --- /dev/null +++ b/sql/00012.sql @@ -0,0 +1,166 @@ +CREATE OR REPLACE PROCEDURE add_nodes(p_user_id int4, p_client_uuid varchar, p_nodes jsonb) +LANGUAGE PLPGSQL AS $$ + +DECLARE + node_data jsonb; + node_updated timestamptz; + db_updated timestamptz; + db_uuid bpchar; + db_client bpchar; + db_client_seq int; + node_uuid bpchar; + parent_uuid_nullable bpchar; + +BEGIN + RAISE NOTICE '--------------------------'; + FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes) + LOOP + node_uuid = (node_data->>'UUID')::bpchar; + node_updated = (node_data->>'Updated')::timestamptz; + + IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' THEN + parent_uuid_nullable = NULL; + ELSE + parent_uuid_nullable = node_data->>'ParentUUID'; + END IF; + + /* Retrieve the current modified timestamp for this node from the database. */ + SELECT + uuid, updated, client, client_sequence + INTO + db_uuid, db_updated, db_client, db_client_seq + FROM public."node" + WHERE + user_id = p_user_id AND + uuid = node_uuid; + + /* Is the node not in database? It needs to be created. */ + IF db_uuid IS NULL THEN + RAISE NOTICE '01 New node %', node_uuid; + INSERT INTO public."node" ( + user_id, "uuid", parent_uuid, created, updated, + "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + VALUES( + p_user_id, + node_uuid, + parent_uuid_nullable, + (node_data->>'Created')::timestamptz, + (node_data->>'Updated')::timestamptz, + (node_data->>'Name')::varchar, + (node_data->>'Content')::text, + (node_data->>'Markdown')::bool, + '', /* content_encrypted */ + p_client_uuid, + (node_data->>'ClientSequence')::int + ); + CONTINUE; + END IF; + + + /* The client could send a specific node again if it didn't receive the OK from this procedure before. */ + IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN + RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq; + CONTINUE; + END IF; + + /* Determine if the incoming node data is to go into history or replace the current node. */ + IF db_updated > node_updated THEN + RAISE NOTICE '02 DB newer, % > % (%))', db_updated, node_updated, node_uuid; + /* Incoming node is going straight to history since it is older than the current node. */ + INSERT INTO node_history( + user_id, "uuid", parents, created, updated, + "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + VALUES( + p_user_id, + node_uuid, + (jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors", + (node_data->>'Created')::timestamptz, + (node_data->>'Updated')::timestamptz, + (node_data->>'Name')::varchar, + (node_data->>'Content')::text, + (node_data->>'Markdown')::bool, + '', /* content_encrypted */ + p_client_uuid, + (node_data->>'ClientSequence')::int + ) + ON CONFLICT (client, client_sequence) + DO NOTHING; + ELSE + RAISE NOTICE '03 Client newer, % > % (%, %)', node_updated, db_updated, node_uuid, (node_data->>'ClientSequence'); + /* Incoming node is newer and will replace the current node. + * + * The current node is copied to the node_history table and then modified in place + * with the incoming data. */ + INSERT INTO node_history( + user_id, "uuid", parents, + created, updated, "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + SELECT + user_id, + "uuid", + ( + WITH RECURSIVE nodes AS ( + SELECT + uuid, + COALESCE(parent_uuid, '') AS parent_uuid, + name, + 0 AS depth + FROM node + WHERE + uuid = node_uuid + + UNION + + SELECT + n.uuid, + COALESCE(n.parent_uuid, '') AS parent_uuid, + n.name, + nr.depth+1 AS depth + FROM node n + INNER JOIN nodes nr ON n.uuid = nr.parent_uuid + ) + SELECT ARRAY ( + SELECT name + FROM nodes + ORDER BY depth DESC + OFFSET 1 /* discard itself */ + ) + ), + created, + updated, + name, + content, + markdown, + content_encrypted, + client, + client_sequence + FROM public."node" + WHERE + user_id = p_user_id AND + uuid = node_uuid + ON CONFLICT (client, client_sequence) + DO NOTHING; + + /* Current node in database is updated with incoming data. */ + UPDATE public."node" + SET + updated = (node_data->>'Updated')::timestamptz, + updated_seq = nextval('node_updates'), + name = (node_data->>'Name')::varchar, + content = (node_data->>'Content')::text, + markdown = (node_data->>'Markdown')::bool, + client = p_client_uuid, + client_sequence = (node_data->>'ClientSequence')::int + WHERE + user_id = p_user_id AND + uuid = node_uuid; + END IF; + + END LOOP; +END +$$; diff --git a/static/css/notes2.css b/static/css/notes2.css index 31e1f1f..71737dd 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -52,51 +52,51 @@ html { #tree { grid-area: tree; display: grid; - background-color: #ffffff; + background-color: #fafafa; color: #444; z-index: 100; - border-right: 2px solid #ddd; + border-right: 1px solid #ddd; + + n2-tree { + /*border: 2px solid #f8f8f8;*/ + padding: 16px 48px 16px 24px; + } + + &:focus-within { + n2-tree { + /* + border: 2px solid #fe5f55; + */ + } + + } + #logo { display: grid; - grid-template-columns: min-content 1fr min-content; - align-items: center; - justify-items: start; + position: relative; + justify-items: center; + margin-top: 8px; + margin-bottom: 8px; + margin-left: 24px; + margin-right: 24px; cursor: pointer; - padding: 16px; - border-bottom: 1px solid #ccc; - .el-search { - justify-self: end; - } + img { + width: 128px; + left: -20px; - img:first-child { - height: 24px; - margin-right: 8px; } } .icons { display: flex; justify-content: center; - margin: 16px 0px 32px 0px; + margin-bottom: 32px; gap: 8px; } - n2-tree { - .el-treenodes { - margin: 32px; - } - } - - &:focus-within { - n2-tree { - } - - } - - .node { display: grid; grid-template-columns: 40px min-content; @@ -145,27 +145,14 @@ html { } } -[id^="page-"] { - display: none; -} - -#main-page { - display: contents; - - &.node { - #page-node { - display: contents; - } - } - - &.storage { - #page-storage { - display: contents; - n2-pagestorage { - grid-area: content; - } - } - } +#tree-nodes { + padding: 16px 32px; + /* + border-radius: 8px; +*/ + /* + box-shadow: 5px 5px 10px -5px rgba(0, 0, 0, 0.75); + */ } #crumbs { @@ -329,6 +316,7 @@ n2-nodeui { margin-bottom: 32px; &:invalid { + background: #f5f5f5; padding-top: 16px; } } diff --git a/static/favicon.ico b/static/favicon.ico deleted file mode 100644 index 299310f..0000000 Binary files a/static/favicon.ico and /dev/null differ diff --git a/static/images/collapsed.svg b/static/images/collapsed.svg index db06415..d93f4ca 100644 --- a/static/images/collapsed.svg +++ b/static/images/collapsed.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="collapsed.svg" - inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" + inkscape:version="1.4.2 (ebf0e94, 2025-05-08)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -23,13 +23,13 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" - inkscape:zoom="19.349237" - inkscape:cx="11.809251" - inkscape:cy="6.3051583" - inkscape:window-width="1093" - inkscape:window-height="1401" - inkscape:window-x="2560" - inkscape:window-y="0" + inkscape:zoom="4.8373092" + inkscape:cx="6.201795" + inkscape:cy="-12.40359" + inkscape:window-width="1916" + inkscape:window-height="1161" + inkscape:window-x="0" + inkscape:window-y="18" inkscape:window-maximized="1" inkscape:current-layer="layer1" showguides="false" /> @@ -42,13 +42,9 @@ transform="translate(-102.39375,-146.31458)">
${content}
\n` + return `${content}
\n` }, list(token) { @@ -138,7 +134,7 @@ export class MarkedPosition { }, listitem(token) { - return ``
+ return ``
+ (token.escaped ? code : escapeHtmlEntities(code, true))
+ '
\n'
}
- return `'
+ (token.escaped ? code : escapeHtmlEntities(code, true))
@@ -161,7 +157,7 @@ export class MarkedPosition {
blockquote(token) {
const body = this.parser.parse(token.tokens)
- return `\n${body}
\n`
+ return `\n${body}
\n`
},
html(token) {
@@ -173,11 +169,11 @@ export class MarkedPosition {
},
hr(token) {
- return `
\n`
+ return `
\n`
},
checkbox(token) {
- return ` '
},
@@ -222,7 +218,7 @@ export class MarkedPosition {
if (token.tokens.length > 0) {
const start = token.tokens[0].position.start.offset
const end = token.tokens[0].position.end.offset
- ofs = `ondblclick="setpos(event)" data-offset-start="${start}" data-offset-end="${end}"`
+ ofs = `onclick="setpos(event)" data-offset-start="${start}" data-offset-end="${end}"`
}
const content = this.parser.parseInline(token.tokens);
@@ -234,23 +230,23 @@ export class MarkedPosition {
},
strong(token) {
- return `${this.parser.parseInline(token.tokens)}`
+ return `${this.parser.parseInline(token.tokens)}`
},
em(token) {
- return `${this.parser.parseInline(token.tokens)}`
+ return `${this.parser.parseInline(token.tokens)}`
},
codespan(token) {
- return `${escapeHtmlEntities(token.text, true)}`
+ return `${escapeHtmlEntities(token.text, true)}`
},
br(token) {
- return `
`
+ return `
`
},
del(token) {
- return `${this.parser.parseInline(token.tokens)}`
+ return `${this.parser.parseInline(token.tokens)}`
},
link(token) {
@@ -260,7 +256,7 @@ export class MarkedPosition {
return text
}
token.href = cleanHref
- let out = ''
+ out += '>'
return out
},
@@ -293,34 +291,8 @@ export class MarkedPosition {
}
})
- }// }}}}}}
+ }// }}}
parse(text) {// {{{
return this.marked.parse(text)
}// }}}
- async whenElementExist(id) {// {{{
- // The element could have already been created.
- const element = document.getElementById(id)
- if (element) {
- return element
- }
-
- const observer = new MutationObserver((_mutations, observer) => {
- const target = document.getElementById(id)
- if (target) {
- observer.disconnect()
- return target
- }
- })
-
- observer.observe(document.documentElement, {
- childList: true,
- subtree: true
- })
- }// }}}
- async populateImg(fileID, elementID) {// {{{
- let img = await globalThis.nodeStore.files.get(fileID)
- const el = await this.whenElementExist(elementID)
-
- el.src = URL.createObjectURL(img.file)
- }// }}}
}
diff --git a/static/js/page_node.mjs b/static/js/node.mjs
similarity index 75%
rename from static/js/page_node.mjs
rename to static/js/node.mjs
index 753aa46..acebbb7 100644
--- a/static/js/page_node.mjs
+++ b/static/js/node.mjs
@@ -1,15 +1,15 @@
-import { ROOT_NODE, uuidv7 } from 'node_store'
+import { ROOT_NODE } from 'node_store'
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
import { MarkedPosition } from './marked_position.mjs'
-export class N2PageNodeUI extends CustomHTMLElement {
+export class N2NodeUI extends CustomHTMLElement {
static {// {{{
this.tmpl = document.createElement('template')
this.tmpl.innerHTML = `