aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJordan Bracco <href@random.sh>2022-04-07 23:54:23 +0000
committerJordan Bracco <href@random.sh>2022-04-07 23:54:23 +0000
commit6d7887c51ba7664688bc568aa0c8538da0e64e7b (patch)
tree452ded9651d17bdd3fe941f15dfec2325f008db9
guess it was time for an initial commit
-rw-r--r--Makefile24
-rw-r--r--README.md81
-rw-r--r--apps/dreki/include/dreki_plum.hrl26
-rw-r--r--apps/dreki/src/dreki.app.src33
-rw-r--r--apps/dreki/src/dreki.hrl36
-rw-r--r--apps/dreki/src/dreki_app.erl81
-rw-r--r--apps/dreki/src/dreki_config.erl58
-rw-r--r--apps/dreki/src/dreki_dets_store.erl74
-rw-r--r--apps/dreki/src/dreki_dets_tasks.erl90
-rw-r--r--apps/dreki/src/dreki_error.erl52
-rw-r--r--apps/dreki/src/dreki_event_manager.erl32
-rw-r--r--apps/dreki/src/dreki_id.erl13
-rw-r--r--apps/dreki/src/dreki_node.erl103
-rw-r--r--apps/dreki/src/dreki_node_server.erl21
-rw-r--r--apps/dreki/src/dreki_peer_service.erl32
-rw-r--r--apps/dreki/src/dreki_plum.erl120
-rw-r--r--apps/dreki/src/dreki_store.erl429
-rw-r--r--apps/dreki/src/dreki_store_backend.erl33
-rw-r--r--apps/dreki/src/dreki_store_namespace.erl14
-rw-r--r--apps/dreki/src/dreki_sup.erl39
-rw-r--r--apps/dreki/src/dreki_task.erl58
-rw-r--r--apps/dreki/src/dreki_tasks.erl141
-rw-r--r--apps/dreki/src/dreki_tasks_cloyster.erl21
-rw-r--r--apps/dreki/src/dreki_tasks_script.erl24
-rw-r--r--apps/dreki/src/dreki_uri.erl1
-rw-r--r--apps/dreki/src/dreki_urn.erl173
-rw-r--r--apps/dreki/src/dreki_world.erl366
-rw-r--r--apps/dreki/src/dreki_world_dns.erl162
-rw-r--r--apps/dreki/src/dreki_world_plum_events.erl17
-rw-r--r--apps/dreki/src/dreki_world_server.erl63
-rw-r--r--apps/dreki/src/dreki_world_store.erl48
-rw-r--r--apps/dreki/src/dreki_world_tasks.erl66
-rw-r--r--apps/dreki_web/.gitignore20
-rw-r--r--apps/dreki_web/Makefile93
-rw-r--r--apps/dreki_web/README.md9
-rw-r--r--apps/dreki_web/assets/app.css30
-rw-r--r--apps/dreki_web/assets/app.js9
-rw-r--r--apps/dreki_web/assets/images/android-chrome-192x192.pngbin0 -> 11765 bytes
-rw-r--r--apps/dreki_web/assets/images/android-chrome-512x512.pngbin0 -> 38619 bytes
-rw-r--r--apps/dreki_web/assets/images/apple-touch-icon.pngbin0 -> 10450 bytes
-rw-r--r--apps/dreki_web/assets/images/favicon-16x16.pngbin0 -> 659 bytes
-rw-r--r--apps/dreki_web/assets/images/favicon-32x32.pngbin0 -> 1322 bytes
-rw-r--r--apps/dreki_web/assets/images/favicon.icobin0 -> 15406 bytes
-rw-r--r--apps/dreki_web/assets/lib/controllers/graphviz_controller.js57
-rw-r--r--apps/dreki_web/assets/package-lock.json3603
-rw-r--r--apps/dreki_web/assets/package.json32
-rw-r--r--apps/dreki_web/assets/postcss.config.js8
-rw-r--r--apps/dreki_web/assets/rebar.lock1
-rw-r--r--apps/dreki_web/assets/tailwind.config.js26
-rw-r--r--apps/dreki_web/package-lock.json6
-rw-r--r--apps/dreki_web/priv/docs_html_fragments/API.html13
-rw-r--r--apps/dreki_web/rebar.config27
-rw-r--r--apps/dreki_web/rebar.lock65
-rw-r--r--apps/dreki_web/src/dreki_web.app.src20
-rw-r--r--apps/dreki_web/src/dreki_web.erl71
-rw-r--r--apps/dreki_web/src/dreki_web_admin_tasks.erl30
-rw-r--r--apps/dreki_web/src/dreki_web_admin_world.erl28
-rw-r--r--apps/dreki_web/src/dreki_web_app.erl68
-rw-r--r--apps/dreki_web/src/dreki_web_auth.erl45
-rw-r--r--apps/dreki_web/src/dreki_web_error.erl20
-rw-r--r--apps/dreki_web/src/dreki_web_index.erl9
-rw-r--r--apps/dreki_web/src/dreki_web_sup.erl35
-rw-r--r--apps/dreki_web/src/dreki_web_task.erl21
-rw-r--r--apps/dreki_web/src/dreki_web_ui.erl43
-rw-r--r--apps/dreki_web/src/dreki_web_ui_error.erl14
-rw-r--r--apps/dreki_web/src/dreki_web_ui_index.erl31
-rw-r--r--apps/dreki_web/src/dreki_web_ui_json_form.erl127
-rw-r--r--apps/dreki_web/src/dreki_web_ui_node.erl23
-rw-r--r--apps/dreki_web/src/dreki_web_ui_stores.erl126
-rw-r--r--apps/dreki_web/src/dreki_web_ui_task.erl20
-rw-r--r--apps/dreki_web/src/dreki_web_ui_tasks.erl14
-rw-r--r--apps/dreki_web/templates/error.dtl8
-rw-r--r--apps/dreki_web/templates/index.dtl38
-rw-r--r--apps/dreki_web/templates/layout.dtl84
-rw-r--r--apps/dreki_web/templates/namespace.dtl6
-rw-r--r--apps/dreki_web/templates/store_list.dtl9
-rw-r--r--apps/dreki_web/templates/store_new.dtl18
-rw-r--r--apps/dreki_web/templates/store_show.dtl8
-rw-r--r--apps/dreki_web/templates/tags/loading7
-rw-r--r--apps/dreki_web/templates/task.dtl106
-rw-r--r--apps/dreki_web/templates/tasks.dtl51
-rw-r--r--apps/dreki_web/templates/world_graph_dot.dtl11
-rw-r--r--config/sys.config107
-rw-r--r--config/vm.args17
-rw-r--r--rebar.config84
-rw-r--r--rebar.lock256
86 files changed, 8085 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..74cedaf
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,24 @@
+DOCS_SRC=doc
+DOCS_TARGET=apps/dreki_web/priv/docs_html_fragments
+TARGETS=compile
+
+compile:
+ rebar3 compile
+
+.PHONY: compile
+
+# -- #
+
+DOCS_SRCS!=(find $(DOCS_SRC) -name "*.md")
+.for _t in ${DOCS_SRCS}
+${_t:S/^${DOCS_SRC}/${DOCS_TARGET}/:S/.md$/.html/}: ${_t}
+ @mkdir -p ${.TARGET:H}
+ kramdown ${.ALLSRC:[1]} > ${.TARGET}
+DOCS_TARGETS+=${_t:S/^${DOCS_SRC}/${DOCS_TARGET}/:S/.md$/.html/}
+.endfor
+
+docs: $(DOCS_TARGETS)
+all: $(TARGETS) $(DOCS_TARGETS)
+
+.MAIN: all
+.PHONY: docs all
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6a4464b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,81 @@
+dreki
+=====
+
+**Dreki manages infrastructure.**
+
+Each of your physical nodes should run Dreki, and that makes the "world".
+
+Terminology:
+
+ * `world` the overall "cluster"
+ * `node` a machine, jail, container, vm..
+ * `peer` a dreki running on a node
+
+Dreki "core" is made of:
+
+* Partisan distribution
+* A global eventually consistent store (plum_db)
+* Resolvable URNs
+* Simple store abstraction
+
+URNs address everything in Dreki and allows to mix multiple kind of data sources (called stores).
+
+Currently, two kind of stores exists:
+
+* The plum_db store, called "global", eventually consistent and replicated everywhere,
+* Mnesia/DETS local store
+
+All stores are queryable from all nodes, resorting to Partisan RPC if the store is local.
+
+The world is a hierarchical acyclic graph, based on DNS. Members of World are regions and nodes:
+
+```
+world.domain
+|- eu
+ |- fr
+ | |- ovh
+ | | |- sbg1
+ | | | `- **node1**
+ | | |`- rbx1
+ | | | `- **node2**
+ | |- scw
+ | | |- dc2
+ | | | `- **node3**
+ | | `- dc5
+ | | |- **node4**
+ |- de
+ | ` hzn
+ | | |- fsn1
+ | | | `- **node6**
+ |- ch
+ | `- exo
+ | `- gnv
+ | `- **node5**
+```
+
+Each region must define a or many member entries: `_dreki.region IN SRV 0 0 1337 sub.region`, `_dreki.region IN SRV 0 0 1337 node.region`.
+
+A region can be given a friendly name by defining in `_dreki.region IN TXT "Some cute name"`.
+
+A node is defined by creating a node entry: `_node._dreki.name IN SRV 0 0 PORT dns.name.`, and:
+* can be given a friendly name by defining `_name._dreki.name IN TXT "Some cute name",
+* can be given a node name (erlang) by defining: `_node._name._dreki.name IN TXT "dreki2@node2"` (usually not needed!).
+
+Dreki uses **partisan** as membership system and uses multiple databases:
+
+* `plum_db` (world-wide, eventually consistent) for world metadata (called <<"global">>)
+* ?? ra ??
+* `mnesia` locally (usually called <<"local">>)
+* whatever you want :) see `dreki_store` and `dreki_store_backend` behaviour!
+
+TODO: A consistent K/V store backed by Ra, automagically made for each region!
+
+Everything in Dreki is addressable and addressed by Dreki URNs (see `doc/URNs.md`).
+
+A dreki peer may represent a node and may create nodes.
+
+Stores:
+
+* "inf.random.sh::qqch::id"
+* "inf.random.sh:eu:fr:scw:dc2::qqch::id"
+*
diff --git a/apps/dreki/include/dreki_plum.hrl b/apps/dreki/include/dreki_plum.hrl
new file mode 100644
index 0000000..bea65aa
--- /dev/null
+++ b/apps/dreki/include/dreki_plum.hrl
@@ -0,0 +1,26 @@
+-define(PLUM_DB_REGIONS_TAB, regions).
+-define(PLUM_DB_NODES_TAB, nodes).
+-define(PLUM_DB_PATHS_TAB, paths).
+-define(PLUM_DB_STORES_TAB, dreki_stores).
+
+%% dreki_store tabs
+-define(PLUM_DB_STORE_TASKS_TAB, dreki_tasks).
+
+%% indices tabs
+-define(PLUM_DB_IDX_ROLE_TAB, 'dreki_idx:roles').
+-define(PLUM_DB_IDX_TAGS_TAB, 'dreki_idx:tags').
+
+-define(PLUM_DB_PREFIXES, [
+ {?PLUM_DB_REGIONS_TAB, ram_disk},
+ {?PLUM_DB_NODES_TAB, ram_disk},
+ {?PLUM_DB_PATHS_TAB, ram_disk},
+ {?PLUM_DB_STORES_TAB, ram_disk},
+
+ %% Stores
+ {?PLUM_DB_STORE_TASKS_TAB, ram_disk},
+
+ %% idxs
+ {?PLUM_DB_IDX_ROLE_TAB, disk},
+ {?PLUM_DB_IDX_TAGS_TAB, disk}
+]).
+
diff --git a/apps/dreki/src/dreki.app.src b/apps/dreki/src/dreki.app.src
new file mode 100644
index 0000000..78009bf
--- /dev/null
+++ b/apps/dreki/src/dreki.app.src
@@ -0,0 +1,33 @@
+{application, dreki,
+ [{description, "An OTP application"},
+ {vsn, "0.1.0"},
+ {registered, []},
+ {mod, {dreki_app, []}},
+ {applications,
+ [kernel,
+ stdlib,
+ mnesia,
+ logger_colorful,
+ opentelemetry,
+ opentelemetry_api,
+ opentelemetry_exporter,
+ uuid,
+ mnesia_rocksdb,
+ plum_db,
+ ra,
+ khepri,
+%% erldns,
+ ory,
+ jsone,
+ jesse,
+ jsx,
+ yamerl,
+ datalog,
+ dreki_web
+ ]},
+ {env,[]},
+ {modules, []},
+
+ {licenses, ["Apache 2.0"]},
+ {links, []}
+ ]}.
diff --git a/apps/dreki/src/dreki.hrl b/apps/dreki/src/dreki.hrl
new file mode 100644
index 0000000..f1a8815
--- /dev/null
+++ b/apps/dreki/src/dreki.hrl
@@ -0,0 +1,36 @@
+-type dreki_id() :: binary().
+-type dreki_urn() :: binary().
+-type dreki_uri() :: dreki_urn() | binary().
+-type dreki_domain() :: binary().
+-type dreki_infrastructure_domain() :: dreki_domain().
+
+-type dreki_kind() :: task | node | region.
+-type dreki_stores() :: tasks | nodes | regions.
+
+-type dreki_expanded_uri_resource() :: dreki_xuri_resource() | dreki_xuri_store() | dreki_xuri_resolve().
+-type dreki_xuri_resource() :: #{resource := #{id := dreki_id(), kind := dreki_stores(), store := dreki_id()}}.
+-type dreki_xuri_store() :: #{store := #{id := dreki_id(), kind := dreki_stores()}}.
+-type dreki_xuri_resolve() :: #{resolve := #{id := dreki_id(), kind := dreki_stores()}}.
+
+-type dreki_expanded_uri() :: #{
+ uri := dreki_uri(),
+ domain := dreki_infrastructure_domain(),
+ kind := dreki_kind(),
+ path := dreki_uri(),
+ resource := dreki_expanded_uri_resource()
+}.
+
+-type dreki_task_handler() :: dreki_task_v1.
+-record(dreki_task, {
+ id :: dreki_id(),
+ uri :: dreki_uri(),
+ handler=dreki_task_v1 :: dreki_task_handler(),
+ description :: undefined | binary(),
+ roles=[] :: [binary()],
+ tags=[] :: [binary()],
+ params=#{} :: #{},
+ persisted=false :: boolean(),
+ dirty=false :: boolean()
+}).
+-type dreki_task() :: #dreki_task{}.
+
diff --git a/apps/dreki/src/dreki_app.erl b/apps/dreki/src/dreki_app.erl
new file mode 100644
index 0000000..a1259a7
--- /dev/null
+++ b/apps/dreki/src/dreki_app.erl
@@ -0,0 +1,81 @@
+%%%-------------------------------------------------------------------
+%% @doc dreki public API
+%% @end
+%%%-------------------------------------------------------------------
+
+-module(dreki_app).
+-behaviour(application).
+
+-include_lib("kernel/include/logger.hrl").
+
+-export([start/2, stop/1]).
+
+start(Type, Args) ->
+ ok = before_start(Type, Args),
+ case dreki_sup:start_link() of
+ {ok, Pid} ->
+ ok = after_start(),
+ {ok, Pid};
+ Error ->
+ Error
+ end.
+
+stop(_State) ->
+ dreki_tasks:stop(),
+ ok.
+
+%% internal functions
+
+before_start(Type, Args) ->
+ logger:set_application_level(dreki, debug),
+ logger:set_application_level(dreki_web, debug),
+ logger:set_application_level(partisan, info),
+ logger:set_application_level(plum_db, info),
+ ?LOG_NOTICE(#{message => "Dreki starting...."}),
+ application:stop(partisan),
+ ok = dreki_config:init(Args),
+ ok = dreki_plum:before_start(),
+ ok = maybe_create_mnesia(),
+ {ok, _} = mnesia_rocksdb:register(),
+ ok = dreki_urn:start(),
+ {ok, _} = dreki_world_dns:start(),
+ ?LOG_NOTICE(#{message => "Pre-Start done"}),
+ ok = dreki_plum:after_start(),
+ ok.
+
+after_start() ->
+ %%ok = dreki_plum:after_start(),
+ ok = setup_event_manager(),
+ ok = dreki_store:start(),
+ ?LOG_NOTICE(#{message => "Dreki Ready"}),
+ dreki_event_manager:notify(dreki_ready),
+ ok.
+
+maybe_create_mnesia() ->
+ ok = maybe_create_mnesia(mnesia:system_info(use_dir)).
+
+maybe_create_mnesia(false) ->
+ ?LOG_NOTICE(#{message => "Creating mnesia directory"}),
+ stopped = mnesia:stop(),
+ ok = mnesia:create_schema([node()]),
+ ok = mnesia:start(),
+ ok;
+maybe_create_mnesia(true) ->
+ ok.
+
+setup_event_manager() ->
+ %% TODO: Alarm handler
+
+ %% We subscribe to partisan up and down events and republish them
+ Mod = partisan_peer_service:manager(),
+
+ Mod:on_up('_', fun(Node) ->
+ dreki_event_manager:notify({peer_up, Node})
+ end),
+
+ Mod:on_down('_', fun(Node) ->
+ dreki_event_manager:notify({peer_down, Node})
+ end),
+
+ ok.
+
diff --git a/apps/dreki/src/dreki_config.erl b/apps/dreki/src/dreki_config.erl
new file mode 100644
index 0000000..69df2d6
--- /dev/null
+++ b/apps/dreki/src/dreki_config.erl
@@ -0,0 +1,58 @@
+-module(dreki_config).
+-include_lib("kernel/include/logger.hrl").
+-include_lib("partisan/include/partisan.hrl").
+-include("dreki_plum.hrl").
+
+-export([init/1]).
+
+-define(CONFIG, [
+ %% All stolen from bondy as partisan isn't that well documented, eh.
+ {partisan, [
+ {broadcast_mods, [plum_db, partisan_plumtree_backend]},
+ {channels, [data, rpc, membership]},
+ {connect_disterl, false},
+ {exchange_tick_period, timer:minutes(1)},
+ {lazy_tick_period, timer:seconds(5)},
+ {parallelism, 4},
+ {membership_strategy, partisan_full_membership_strategy},
+ {partisan_peer_service_manager,
+ partisan_pluggable_peer_service_manager},
+ {pid_encoding, false},
+ {ref_encoding, false},
+ {binary_padding, false},
+ {disable_fast_forward, false},
+ %% Broadcast options
+ {broadcast, false},
+ {tree_refresh, 1000},
+ {relay_ttl, 5}
+ ]},
+
+ %% Same!
+ {plum_db, [
+ {aae_enabled, true},
+ {wait_for_aae_exchange, true},
+ {store_open_retries_delay, 2000},
+ {store_open_retry_limit, 30},
+ {data_exchange_timeout, 60000},
+ {hashtree_timer, 10000},
+ {data_dir, "data/plumdb"},
+ {partitions, 8},
+ {prefixes, ?PLUM_DB_PREFIXES}
+ ]}
+]).
+
+-define(PT, dreki_config_cache).
+
+init(_Args) ->
+ persistent_term:put(?PT, application:get_all_env(dreki)),
+ ok = set_app_configs(?CONFIG),
+ ?LOG_INFO(#{message => "Configured Dreki and dependencies"}),
+ ok = partisan_config:init(),
+ ok.
+
+set_app_configs(Configs) ->
+ lists:foreach(fun ({App, Params}) ->
+ [application:set_env(App, Key, Value) || {Key, Value} <- Params]
+ end, Configs),
+ ok.
+
diff --git a/apps/dreki/src/dreki_dets_store.erl b/apps/dreki/src/dreki_dets_store.erl
new file mode 100644
index 0000000..05cf9fb
--- /dev/null
+++ b/apps/dreki/src/dreki_dets_store.erl
@@ -0,0 +1,74 @@
+-module(dreki_dets_store).
+
+-include("dreki.hrl").
+
+-behaviour(dreki_store_backend).
+
+-export([start/0, start/4, checkout/1, checkin/1, stop/0, stop/1]).
+-export([valid_store/5]).
+-export([list/1, count/1, exists/2, get/2, create/2, update/2, delete/2]).
+
+-record(?MODULE, {tab, file}).
+
+start() -> ok.
+
+valid_store(_Namespace, _Location, _Name, _NSMod, _Args) -> ok.
+
+start(Namespace, Name, _XUrn, Args) ->
+ StoreName = {?MODULE, Namespace, Name},
+ File = maps:get(file_name, Args, <<Namespace/binary, ".", Name/binary, ".dets">>),
+ FileName = binary:bin_to_list(File),
+ case dets:open_file(StoreName, [{file, FileName}, {keypos, 2}]) of
+ {ok, Tab} -> {ok, #?MODULE{tab = Tab, file = FileName}};
+ {error, Error} -> {error, {dets_open_failed, Error}}
+ end.
+
+checkout(#?MODULE{tab = Tab, file = FileName}) ->
+ case dets:open_file(Tab, [{file, FileName}, {keypos, 2}]) of
+ {ok, Tab} -> {ok, {dreki_dets_store_ref, Tab}};
+ {error, Error} -> {error, {dets_open_failed, Error}}
+ end.
+
+checkin({dreki_dets_store_ref, Tab}) ->
+ dets:close(Tab).
+
+stop() -> ok.
+
+stop(#?MODULE{tab = Tab}) ->
+ dets:close(Tab).
+
+list({dreki_dets_store_ref, Tab}) ->
+ dets:foldl(fun (T, {ok, Ts}) -> {ok, [T | Ts]} end, {ok, []}, Tab).
+
+count({dreki_dets_store_ref, Tab}) ->
+ dets:foldl(fun (_T, {ok, Ct}) -> {ok, Ct + 1} end, {ok, 0}, Tab).
+
+exists({dreki_dets_store_ref, Tab}, Id) ->
+ dets:member(Tab, Id).
+
+get({dreki_dets_store_ref, Tab}, Id) ->
+ case dets:lookup(Tab, Id) of
+ [] -> {error, {task_not_found, Id}};
+ [Task] -> {ok, Task}
+ end.
+
+delete({dreki_dets_store_ref, Tab}, Id) ->
+ dets:delete(Tab, Id).
+
+create({dreki_dets_store_ref, Tab}, Task = #dreki_task{persisted=false}) ->
+ case dets:insert_new(Tab, Task#dreki_task{persisted=true, dirty=false}) of
+ true -> {ok, Task#dreki_task{persisted=true, dirty=false}};
+ false -> {error, {task_exists, Task#dreki_task.id}};
+ {error, Error} -> {error, Error}
+ end.
+
+update(_Tab, Task = #dreki_task{persisted=false}) ->
+ {error, {task_not_created, Task#dreki_task.id}};
+update(_Tab, Task = #dreki_task{dirty=false}) ->
+ {ok, Task};
+update({dreki_dets_store_ref, Tab}, Task = #dreki_task{persisted=true, dirty=true}) ->
+ case dets:insert(Tab, Task#dreki_task{dirty=false}) of
+ ok -> {ok, Task#dreki_task{dirty=false}};
+ {error, Error} -> {error, Error}
+ end.
+
diff --git a/apps/dreki/src/dreki_dets_tasks.erl b/apps/dreki/src/dreki_dets_tasks.erl
new file mode 100644
index 0000000..9b798ef
--- /dev/null
+++ b/apps/dreki/src/dreki_dets_tasks.erl
@@ -0,0 +1,90 @@
+-module(dreki_dets_tasks).
+-include("dreki.hrl").
+
+%% Types
+-type db() :: term().
+-type args() :: #{db_name => binary()}.
+
+%% storage-specific
+-export([start/1, open/1, sync/1, close/1, stop/1]).
+-export([checkout/1, checkin/1]).
+-export([list/1, count/1, exists/2, get/2, create/2, update/2, delete/2]).
+
+-define(DETS, dreki_tasks_dets).
+
+dets_name(_Args = #{store_name := StoreName}) when is_binary(StoreName) ->
+ {dreki_dets_tasks, StoreName}.
+
+file_name(_Args = #{file_name := FileName}) when is_binary(FileName) ->
+ binary:bin_to_list(FileName);
+file_name(_Args = #{store_name := StoreName}) when is_binary(StoreName) ->
+ File = <<"tasks.", StoreName/binary, ".dets">>,
+ binary:bin_to_list(File).
+
+-spec start(args()) -> {ok, db()} | {error, Reason::term()}.
+start(Args) ->
+ dets:open_file(dets_name(Args), [{file, file_name(Args)}, {keypos, 2}]).
+
+-spec open(args()) -> {ok, db()} | {error, Reason::term()}.
+open(Args) ->
+ start(Args).
+
+-spec close(db()) -> ok | {error, Reason::term()}.
+close(Tab) ->
+ dets:close(Tab).
+
+-spec stop(args()) -> ok | {error, Reason::term()}.
+stop(Args) ->
+ Tab = dets_name(Args),
+ dets:close(Tab).
+
+sync(Tab) ->
+ dets:sync(Tab).
+
+checkout(Args) ->
+ start(Args).
+
+checkin(Tab) ->
+ dets:close(Tab).
+
+-spec list(db()) -> {ok, [dreki_task()]} | {error, Reason::term()}.
+list(Tab) ->
+ dets:foldl(fun (T, {ok, Ts}) -> {ok, [T | Ts]} end, {ok, []}, Tab).
+
+-spec count(db()) -> {ok, Count::non_neg_integer()} | {error, Reason::term()}.
+count(Tab) ->
+ dets:foldl(fun (_T, {ok, Ct}) -> {ok, Ct + 1} end, {ok, 0}, Tab).
+
+-spec exists(db(), dreki_id()) -> boolean.
+exists(Tab, Id) ->
+ dets:member(Tab, Id).
+
+-spec get(db(), dreki_id()) -> {ok, dreki_task()} | {error, {task_not_found, dreki_id()}}.
+get(Tab, Id) ->
+ case dets:lookup(Tab, Id) of
+ [] -> {error, {task_not_found, Id}};
+ [Task] -> {ok, Task}
+ end.
+
+-spec delete(db(), dreki_id()) -> ok | {error, Reason::term()}.
+delete(Tab, Id) ->
+ dets:delete(Tab, Id).
+
+-spec create(db(), dreki_task()) -> {ok, dreki_task()} | {error, Reason::term()}.
+create(Tab, Task = #dreki_task{persisted=false}) ->
+ case dets:insert_new(Tab, Task#dreki_task{persisted=true, dirty=false}) of
+ true -> {ok, Task#dreki_task{persisted=true, dirty=false}};
+ false -> {error, {task_exists, Task#dreki_task.id}};
+ {error, Error} -> {error, Error}
+ end.
+
+-spec update(db(), dreki_task()) -> {ok, dreki_task()} | {error, Reason::term()}.
+update(_Tab, Task = #dreki_task{persisted=false}) ->
+ {error, {task_not_created, Task#dreki_task.id}};
+update(_Tab, Task = #dreki_task{dirty=false}) ->
+ {ok, Task};
+update(Tab, Task = #dreki_task{persisted=true, dirty=true}) ->
+ case dets:insert(Tab, Task#dreki_task{dirty=false}) of
+ ok -> {ok, Task#dreki_task{dirty=false}};
+ {error, Error} -> {error, Error}
+ end.
diff --git a/apps/dreki/src/dreki_error.erl b/apps/dreki/src/dreki_error.erl
new file mode 100644
index 0000000..1ca2125
--- /dev/null
+++ b/apps/dreki/src/dreki_error.erl
@@ -0,0 +1,52 @@
+-module(dreki_error).
+-compile({no_auto_import,[error/3]}).
+-export([errors/0, error/1, error/2, error/3, error/4, error/5]).
+-export([as_map/1]).
+
+-record(?MODULE, {
+ code :: atom(),
+ status = 500,
+ title :: binary(),
+ detail = undefined :: undefined | binary(),
+ source = [] :: [source]
+}).
+
+-type t() :: #?MODULE{}.
+
+-type source() :: {urn, dreki_urn:urn()} | {pointer, JSONPointer :: binary()} | {parameter, binary()} | {binary(), binary()}.
+
+errors() -> [
+ #?MODULE{code = exists, status = 409, title = <<"Already exists">>},
+ #?MODULE{code = not_found, status = 404, title = <<"Not Found">>},
+ #?MODULE{code = error, status = 500, title = <<"Error">>},
+ #?MODULE{code = store_start_failed, status = 500, title = <<"Store start failed">>}
+].
+
+error(Code) ->
+ error(Code, undefined, []).
+
+error(Code, Source) when is_list(Source) ->
+ error(Code, undefined, Source);
+error(Code, Detail) when is_binary(Detail) ->
+ error(Code, Detail, []).
+
+error(Code, Detail, Source) when is_binary(Detail) or is_atom(Detail) and is_list(Source) ->
+ {error, case lists:keyfind(Code, 2, errors()) of
+ Error = #?MODULE{} -> Error#?MODULE{detail = Detail, source = Source};
+ _ -> #?MODULE{code = error, status = 500, title = <<"Error">>, detail = Detail, source = Source}
+ end};
+
+error(Code, Status, Title) when is_atom(Code) and is_integer(Status) and is_binary(Title) ->
+ {error, #?MODULE{code = Code, status = Status, title = Title}}.
+
+error(Code, Status, Title, Detail) when is_integer(Status) and is_binary(Title) and is_binary(Detail) ->
+ {error, #?MODULE{code = Code, status = Status, title = Title, detail = Detail}};
+error(Code, Status, Title, Source) when is_integer(Status) and is_binary(Title) and is_list(Source) ->
+ {error, #?MODULE{code = Code, status = Status, title = Title, source = Source}}.
+
+error(Code, Status, Title, Detail, Source) when is_integer(Status) and is_binary(Title) and is_list(Source) and is_binary(Detail) ->
+ {error, #?MODULE{code = Code, status = Status, title = Title, detail = Detail, source = Source}}.
+
+as_map(#?MODULE{code = Code, status = Status, title = Title, detail = Detail, source = Source}) ->
+ #{code => Code, status => Status, title => Title, detail => Detail, source => Source}.
+
diff --git a/apps/dreki/src/dreki_event_manager.erl b/apps/dreki/src/dreki_event_manager.erl
new file mode 100644
index 0000000..a392692
--- /dev/null
+++ b/apps/dreki/src/dreki_event_manager.erl
@@ -0,0 +1,32 @@
+-module(dreki_event_manager).
+-include_lib("kernel/include/logger.hrl").
+-define(SERVER, {local, ?MODULE}).
+-export([start_link/1]).
+-export([add_handler/2]).
+-export([notify/1]).
+-export([init/1, handle_event/2]).
+
+start_link(Options) ->
+ case gen_event:start_link(?SERVER, Options) of
+ Ok = {ok, _} ->
+ ok = add_handler(?MODULE, undefined),
+ Ok;
+ Error ->
+ Error
+ end.
+
+add_handler(Handler, Args) ->
+ gen_event:add_handler(?MODULE, Handler, Args).
+
+notify(Event) ->
+ gen_event:notify(?MODULE, Event).
+
+%% gen_event handler
+
+init(_Args) ->
+ {ok, undefined}.
+
+handle_event(Event, State) ->
+ ?LOG_NOTICE(#{event => Event}),
+ {ok, State}.
+
diff --git a/apps/dreki/src/dreki_id.erl b/apps/dreki/src/dreki_id.erl
new file mode 100644
index 0000000..17a3e48
--- /dev/null
+++ b/apps/dreki/src/dreki_id.erl
@@ -0,0 +1,13 @@
+-module(dreki_id).
+
+-export([get/0, valid/1]).
+
+get() ->
+ uuid:get_v4().
+
+valid(MaybeId) ->
+ case re:run(MaybeId, "^[a-z0-9-_.]{3,100}$") of
+ nomatch -> {error, {invalid_dreki_id, MaybeId}};
+ {match, _} -> ok
+ end.
+
diff --git a/apps/dreki/src/dreki_node.erl b/apps/dreki/src/dreki_node.erl
new file mode 100644
index 0000000..87dbc73
--- /dev/null
+++ b/apps/dreki/src/dreki_node.erl
@@ -0,0 +1,103 @@
+-module(dreki_node).
+-include("dreki.hrl").
+-include_lib("opentelemetry_api/include/otel_tracer.hrl").
+-behaviour(partisan_gen_fsm).
+-compile({no_auto_import,[get/0]}).
+-export([get/0, urn/0, stores/0]).
+-export([rpc/4, rpc/5]).
+-export([uri/0]). % deprecated
+-export([parents/0, parents/1, parent/0, parent/1]).
+-export([neighbours/0, neighbours/1]).
+-export([descendants/0, descendants/1]).
+-export([ensure_local_node/0]).
+
+-type rpc_error() :: {rpc_error, dreki_urn(), timeout | any()}.
+
+rpc(Path, Mod, Fun, Args) ->
+ rpc(Path, Mod, Fun, Args, #{timeout => 1000}).
+
+-spec rpc(dreki_urn(), module(), function(), Args :: [], #{timeout => non_neg_integer()}) -> {ok, any()} | {error, rpc_error()}.
+rpc(Path, Mod, Fun, Args, #{timeout := Timeout}) ->
+ ?with_span(<<"dreki_node:rpc">>, #{}, fun(ChildSpanCtx) ->
+ case dreki_world_dns:node_param(dreki_world:path_to_domain(Path), node_name) of
+ {ok, NodeName} ->
+ case partisan_rpc_backend:call(NodeName, Mod, Fun, Args, Timeout) of
+ {badrpc, Error} -> {error, {rpc_error, Path, Error}};
+ Result -> Result
+ end;
+ Error -> Error
+ end
+ end).
+
+get() ->
+ {ok, Node} = dreki_world:get_node(dreki_world:node()),
+ Node.
+
+urn() -> dreki_world:path().
+
+uri() -> urn().
+
+stores() -> dreki_world:stores(uri()).
+
+parents() ->
+ parents(urn()).
+
+parents(Node) ->
+ {ok, #{node := NodeUrn}} = dreki_world:get_node(Node),
+ NodeDomain = dreki_world:path_to_domain(NodeUrn),
+ Vertices0 = dreki_world_dns:get_path({root, dreki_world:internal_domain()}, {node, NodeDomain}),
+ Vertices = lists:map(fun get_from_vertex/1, Vertices0),
+ [_Me | Parents] = lists:reverse(Vertices),
+ Parents.
+
+parent() ->
+ parent(urn()).
+
+parent(Node) ->
+ {ok, #{node := NodeUrn}} = dreki_world:get_node(Node),
+ NodeDomain = dreki_world:path_to_domain(NodeUrn),
+ [Parent] = dreki_world_dns:in_neighbours({node, NodeDomain}),
+ get_from_vertex(Parent).
+
+neighbours() ->
+ neighbours(urn()).
+
+neighbours(Node) ->
+ {ok, #{node := NodeUrn}} = dreki_world:get_node(Node),
+ NodeDomain = dreki_world:path_to_domain(NodeUrn),
+ [Parent] = dreki_world_dns:in_neighbours({node, NodeDomain}),
+ Neighbours = dreki_world_dns:out_neighbours(Parent),
+ lists:map(fun get_from_vertex/1, Neighbours -- [{node, NodeDomain}]).
+
+descendants() ->
+ descendants(urn()).
+
+descendants(Node) ->
+ {ok, #{node := NodeUrn}} = dreki_world:get_node(Node),
+ NodeDomain = dreki_world:path_to_domain(NodeUrn),
+ Descendants0 = dreki_world_dns:out_neighbours({node, NodeDomain}),
+ Descendants = case Descendants0 of
+ [{region, NodeDomain}] -> dreki_world_dns:out_neighbours({region, NodeDomain});
+ D -> D
+ end,
+ lists:map(fun get_from_vertex/1, Descendants).
+
+get_from_vertex({root, Domain}) ->
+ {ok, Region} = dreki_world:get_region_from_dns_name(Domain),
+ Region;
+get_from_vertex({region, Domain}) ->
+ {ok, Region} = dreki_world:get_region_from_dns_name(Domain),
+ Region;
+get_from_vertex({node, Domain}) ->
+ {ok, Node} = dreki_world:get_node_from_dns_name(Domain),
+ Node.
+
+ensure_local_node() ->
+ case dreki_world:get_node(uri()) of
+ {ok, _} -> ok;
+ {error, {not_found, _}} -> create_local_node()
+ end.
+
+create_local_node() ->
+ dreki_world:create_node(uri(), #{}).
+
diff --git a/apps/dreki/src/dreki_node_server.erl b/apps/dreki/src/dreki_node_server.erl
new file mode 100644
index 0000000..c8bca51
--- /dev/null
+++ b/apps/dreki/src/dreki_node_server.erl
@@ -0,0 +1,21 @@
+-module(dreki_node_server).
+-behaviour(partisan_gen_fsm).
+
+-export([start_link/1, send_event/2]).
+-export([init/1]).
+-export([wait/2]).
+
+-record(data, { }).
+
+start_link(Args) ->
+ partisan_gen_fsm:start_link({local, ?MODULE}, ?MODULE, Args, []).
+
+send_event(Name, Event) ->
+ partisan_gen_fsm:send_event(Name, Event).
+
+init(_) ->
+ {ok, wait, #data{}}.
+
+wait(Event, Data) ->
+ logger:info("node_server wait event: ~p", [Event]),
+ {next_state, wait, Data}.
diff --git a/apps/dreki/src/dreki_peer_service.erl b/apps/dreki/src/dreki_peer_service.erl
new file mode 100644
index 0000000..fe69de3
--- /dev/null
+++ b/apps/dreki/src/dreki_peer_service.erl
@@ -0,0 +1,32 @@
+-module(dreki_peer_service).
+-export([join/1, connect_nearest_dns/0]).
+-define(DEFAULT_PORT, 18086).
+
+join(Node) ->
+ connect_node(Node).
+
+connect_nearest_dns() ->
+ %%connect_nearest_dns(dreki_world_dns:parents(dreki_world_dns:node()), []),
+ todo.
+
+connect_nearest_dns([], _) -> error;
+connect_nearest_dns([{root, _} | Rest], Tries) ->
+ Nodes = dreki_world_dns:all_nodes() -- Tries,
+ connect_nearest_dns(Rest ++ Nodes, Tries);
+connect_nearest_dns([V = {region, _} | Rest], Tries) ->
+ connect_nearest_dns(Rest ++ dreki_world_dns:parents(V), Tries);
+connect_nearest_dns([V = {node, Node} | Rest], Tries) ->
+ case connect_node(Node) of
+ ok -> ok;
+ Error ->
+ logger:error("Failed to connect to node: ~p: ~p", [Node, Error]),
+ connect_nearest_dns(Rest, [V | Tries])
+ end.
+
+connect_node(Node) ->
+ {ok, Endpoints} = dreki_world_dns:node_ips(Node),
+ {ok, NodeName} = dreki_world_dns:node_param(Node, node_name),
+ logger:info("dreki_peer_service: joining node=~p ~p ~p", [Node, NodeName, Endpoints]),
+ {ok, NodeSpec} = partisan:node_spec(NodeName, Endpoints),
+ partisan_peer_service:join(NodeSpec).
+
diff --git a/apps/dreki/src/dreki_plum.erl b/apps/dreki/src/dreki_plum.erl
new file mode 100644
index 0000000..9c03e46
--- /dev/null
+++ b/apps/dreki/src/dreki_plum.erl
@@ -0,0 +1,120 @@
+-module(dreki_plum).
+-include_lib("kernel/include/logger.hrl").
+-export([before_start/0, after_start/0]).
+
+%% Mostly copied from bondy_app.erl
+
+before_start() ->
+ %% We temporarily disable plum_db's AAE to avoid rebuilding hashtrees
+ %% until we are ready to do it
+ ok = suspend_aae(),
+ logger:debug("-- DISABLED AAE ! --"),
+ _ = application:ensure_all_started(plum_db, permanent),
+ ok.
+
+after_start() ->
+ %% We need to re-enable AAE (if it was enabled) so that hashtrees
+ %% are build
+ _ = application:ensure_all_started(partisan, permanent),
+ _ = application:ensure_all_started(plum_db, permanent),
+ ok = restore_aae(),
+ ok = maybe_wait_for_plum_db_hashtrees(),
+ ok = maybe_wait_for_aae_exchange(),
+ ok.
+
+suspend_aae() ->
+ case application:get_env(plum_db, aae_enabled, true) of
+ true ->
+ ok = application:set_env(plum_db, priv_aae_enabled, true),
+ ok = application:set_env(plum_db, aae_enabled, false),
+ ?LOG_NOTICE(#{
+ description => "Temporarily disabled active anti-entropy (AAE) during initialisation"
+ }),
+ ok;
+ false ->
+ ok
+ end.
+
+restore_aae() ->
+ case application:get_env(plum_db, priv_aae_enabled, false) of
+ true ->
+ %% plum_db should have started so we call plum_db_config
+ ok = plum_db_config:set(aae_enabled, true),
+ ?LOG_NOTICE(#{
+ description => "Active anti-entropy (AAE) re-enabled"
+ }),
+ ok;
+ false ->
+ ok
+ end.
+
+maybe_wait_for_plum_db_partitions() ->
+ case wait_for_partitions() of
+ true ->
+ %% We block until all partitions are initialised
+ ?LOG_NOTICE(#{
+ description => "Application master is waiting for plum_db partitions to be initialised"
+ }),
+ plum_db_startup_coordinator:wait_for_partitions();
+ false ->
+ ok
+ end.
+
+maybe_wait_for_plum_db_hashtrees() ->
+ case wait_for_hashtrees() of
+ true ->
+ %% We block until all hashtrees are built
+ ?LOG_NOTICE(#{
+ description => "Application master is waiting for plum_db hashtrees to be built"
+ }),
+ plum_db_startup_coordinator:wait_for_hashtrees();
+ false ->
+ ok
+ end,
+
+ %% We stop the coordinator as it is a transcient worker
+ plum_db_startup_coordinator:stop().
+
+maybe_wait_for_aae_exchange() ->
+ %% When plum_db is included in a principal application, the latter can
+ %% join the cluster before this phase and perform a first aae exchange
+ case wait_for_aae_exchange() of
+ true ->
+ MyNode = partisan:node(),
+ Members = partisan_plumtree_broadcast:broadcast_members(),
+
+ case lists:delete(MyNode, Members) of
+ [] ->
+ %% We have not yet joined a cluster, so we finish
+ ok;
+ Peers ->
+ ?LOG_NOTICE(#{
+ description => "Application master is waiting for plum_db AAE to perform exchange"
+ }),
+ %% We are in a cluster, we randomnly pick a peer and
+ %% perform an AAE exchange
+ [Peer|_] = lists_utils:shuffle(Peers),
+ %% We block until the exchange finishes successfully
+ %% or with error, we finish anyway
+ _ = plum_db:sync_exchange(Peer),
+ ok
+ end;
+ false ->
+ ok
+ end.
+
+wait_for_aae_exchange() ->
+ plum_db_config:get(aae_enabled) andalso
+ plum_db_config:get(wait_for_aae_exchange).
+
+wait_for_partitions() ->
+ %% Waiting for hashtrees implies waiting for partitions
+ plum_db_config:get(wait_for_partitions) orelse wait_for_hashtrees().
+
+wait_for_hashtrees() ->
+ %% If aae is disabled the hastrees will never get build
+ %% and we would block forever
+ (
+ plum_db_config:get(aae_enabled)
+ andalso plum_db_config:get(wait_for_hashtrees)
+ ) orelse wait_for_aae_exchange().
diff --git a/apps/dreki/src/dreki_store.erl b/apps/dreki/src/dreki_store.erl
new file mode 100644
index 0000000..ae6fd66
--- /dev/null
+++ b/apps/dreki/src/dreki_store.erl
@@ -0,0 +1,429 @@
+-module(dreki_store).
+-include("dreki.hrl").
+-include("dreki_plum.hrl").
+-include_lib("opentelemetry_api/include/otel_tracer.hrl").
+-define(BACKENDS_PT, {dreki_stores, backends}).
+-compile({no_auto_import,[get/1]}).
+-export([backends/0]).
+-export([start/0]).
+-export([namespaces/0, namespace/1]).
+-export([stores/0, stores_local/0, stores/1, get_store/1, create_store/4, create_store/6]).
+-export([list/1, get/1, new/1, create/2, update/2, delete/1]).
+-export([store_as_map/1]).
+
+-behaviour(dreki_urn).
+-export([expand_urn_resource_rest/4]).
+
+-export([callback/3, list_/2, get_/2, new_/2, create_/3, update_/3, delete_/2]).
+
+-record(store, {urn, xurn, namespace, name, namespace_mod, backend_mod, backend_params}).
+
+-type t() :: #store{}.
+-type store() :: t() | dreki_uri() | dreki_expanded_uri().
+
+backends() -> [dreki_dets_store, dreki_world_store].
+namespaces() -> [{<<"tasks">>, dreki_tasks, #{}}].
+
+namespace(Name) ->
+ lists:keyfind(Name, 1, namespaces()).
+
+start() ->
+ [ok = dreki_urn:register_namespace(NS, ?MODULE, Env) || {NS, _, Env} <- namespaces()],
+ [ok = BackendMod:start() || BackendMod <- backends()],
+ persistent_term:put(?BACKENDS_PT, #{}),
+ {Backends, Errors} = lists:foldr(fun (Store = #store{}, {Acc, Errs}) ->
+ case start_store_(Store) of
+ {ok, Backend} -> {maps:put(Store#store.urn, Backend, Acc), Errs};
+ {error, Err} -> {Acc, [Err | Errs]}
+ end
+ end, {#{}, []}, stores_local()),
+ persistent_term:put(?BACKENDS_PT, Backends),
+ [logger:error("dreki_store: ~p", [Err]) || Err <- Errors],
+ ok.
+
+stores() ->
+ plum_db:fold(fun
+ ({_, Value}, Acc) -> [as_record(Value) | Acc];
+ ({_, [Value]}, Acc) -> [as_record(Value) | Acc];
+ ({_, ['$deleted']}, Acc) -> Acc
+ end, [], {?PLUM_DB_STORES_TAB, '_'}).
+
+stores_local() ->
+ lists:filter(fun is_local/1, stores()).
+
+stores(Namespace) ->
+ case namespace(Namespace) of
+ undefined -> {error, {namespace_not_found, Namespace}};
+ {_, _, _} -> {ok, [Store || Store = #store{namespace = Namespace} <- stores()]}
+ end.
+
+start_store(Store = #store{}) ->
+ case start_store_(Store) of
+ {ok, Backend} ->
+ alarm_handler:clear_alarm({?MODULE, Store#store.urn}),
+ Pt = persistent_term:get(?BACKENDS_PT),
+ persistent_term:put(?BACKENDS_PT, maps:put(Store#store.urn, Backend, Pt)),
+ {ok, Backend};
+ Error ->
+ alarm_handler:set_alarm({?MODULE, Store#store.urn}, {start_failed, Error}),
+ Error
+ end.
+
+start_store_(Store = #store{backend_mod = Mod}) ->
+ case is_local(Store) of
+ true -> start_store__(Store);
+ false -> {error, {store_not_local, Store#store.urn, dreki_node:urn()}}
+ end.
+start_store__(Store) ->
+ case maps:get(Store#store.urn, persistent_term:get(?BACKENDS_PT), undefined) of
+ undefined -> start_store___(Store);
+ _Started -> {error, {store_already_started, Store#store.urn}}
+ end.
+start_store___(Store = #store{backend_mod = Mod}) ->
+ case Mod:start(Store#store.namespace, Store#store.name, Store#store.urn, Store#store.backend_params) of
+ {ok, Backend} -> {ok, Backend};
+ {error, Error} -> {error, {store_start_failed, Store#store.urn, Error}}
+ end.
+
+create_store(Urn, Module, ModuleParams, Params) when is_binary(Urn) ->
+ case dreki_urn:expand(Urn) of
+ Error = {error, _} -> Error;
+ {ok, XUrn} -> create_store(XUrn, Module, ModuleParams, Params)
+ end;
+create_store(#{location := Location, resource := #{directory := #{namespace := NS, directory := Name}}}, Module, ModuleParams, Params) ->
+ create_store(NS, Location, Name, Module, ModuleParams, Params).
+
+create_store(Namespace, Location, Name, Module, ModuleParams, Params) ->
+ case lists:keyfind(Namespace, 1, namespaces()) of
+ undefined -> {error, {namespace_not_found, Namespace}};
+ {_, NSMod, NSEnv} ->
+ case dreki_urn:expand(Location) of
+ Error = {error, _} -> Error;
+ {ok, #{location := Loc}} ->
+ Urn = <<Loc/binary, "::", Namespace/binary, ":", Name/binary>>,
+ case get_store(Urn) of
+ {ok, _} -> dreki_error:error(exists, [{urn, Urn}]);
+ Error = {error, _} -> Error;
+ not_found ->
+ case {NSMod:valid_store(Namespace, Location, Name, Module), Module:valid_store(Namespace, Location, Name, NSMod, ModuleParams)} of
+ {ok, ok} ->
+ {Prefix, Key} = {{?PLUM_DB_STORES_TAB, Loc}, {Namespace, Name}},
+ %%ok = put_path(Urn, store, {Prefix, Key}),
+ ok = plum_db:put(Prefix, Key, #{urn => Urn, name => Name, namespace => Namespace, module => Module, module_params => ModuleParams, params => Params}),
+ get_store(Urn);
+ {{error, Error}, _} -> {error, {store_create_failed_namespace_rejected, {NSMod, Error}}};
+ {_, {error, Error}} -> {error, {store_create_failed_store_rejected, {Module, Error}}}
+ end
+ end
+ end
+ end.
+
+expand_urn_resource_rest(Namespace, ResXUrn, Part, _Env) ->
+ logger:debug("Expanding resource ~p ~p", [Part, ResXUrn]),
+ expand_urn_resource_rest_(Namespace, ResXUrn, binary:split(Part, <<":">>, [global])).
+
+expand_urn_resource_rest_(Namespace, Res = #{namespace := _}, [<<"schemas">>]) ->
+ {ok, Res#{schemas => #{schemas => all}}};
+expand_urn_resource_rest_(Namespace, Res = #{namespace := _}, [<<"schemas">>, Schema]) ->
+ {ok, Res#{schemas => #{schemas => Schema}}};
+expand_urn_resource_rest_(Namespace, Res = #{namespace := _}, [<<"schemas">>, <<>>, <<>>]) ->
+ {ok, Res#{schema => #{schema => default}}};
+expand_urn_resource_rest_(Namespace, Res = #{namespace := _}, [<<"schemas">>, Schema, SchemaVer]) ->
+ {ok, Res#{schema => #{schema => Schema, version => SchemaVer}}};
+expand_urn_resource_rest_(Namespace, Res = #{directory := _}, [<<"schemas">>]) ->
+ {ok, Res#{schemas => #{schemas => all}}};
+expand_urn_resource_rest_(Namespace, Res = #{directory := _}, [<<"schemas">>, Schema]) ->
+ {ok, Res#{schemas => #{schemas => Schema}}};
+expand_urn_resource_rest_(Namespace, Res = #{directory := _}, [<<"schemas">>, <<>>, <<>>]) ->
+ {ok, Res#{schema => #{schema => default}}};
+expand_urn_resource_rest_(Namespace, Res = #{directory := _}, [<<"schemas">>, Schema, SchemaVer]) ->
+ {ok, Res#{schema => #{schema => Schema, version => SchemaVer}}};
+expand_urn_resource_rest_(_, _, _) ->
+ error.
+
+-spec get_store(store()) -> {ok, t()} | {error, any()}.
+get_store(Urn) when is_binary(Urn) ->
+ case dreki_urn:expand(Urn) of
+ {ok, Uri} -> get_store(Uri);
+ Error -> Error
+ end;
+get_store(#{location := Location, resource := #{resource := #{namespace := NS, directory := Dir}}}) ->
+ get_store(Location, NS, Dir);
+get_store(#{location := Location, resource := #{directory := #{namespace := NS, directory := Dir}}}) ->
+ get_store(Location, NS, Dir);
+get_store(Fail) ->
+ {error, {uri_resolution_failed, Fail}}.
+
+get_store(Location, Namespace, Name) ->
+ case plum_db:get({?PLUM_DB_STORES_TAB, Location}, {Namespace, Name}) of
+ undefined -> not_found;
+ ['$deleted'] -> not_found;
+ [Val] -> {ok, as_record(Val)};
+ Val -> {ok, as_record(Val)}
+ end.
+
+as_record([Map]) -> as_record(Map);
+as_record(#{urn := Urn, name := Name, namespace := Namespace, module := Module, module_params := ModuleParams}) ->
+ {ok, XUrn} = dreki_urn:expand(Urn),
+ {_, NSMod, _} = lists:keyfind(Namespace, 1, namespaces()),
+ #store{urn = Urn, xurn = XUrn, name = Name, namespace = Namespace, namespace_mod = NSMod, backend_mod = Module, backend_params = ModuleParams}.
+
+store_as_map(#store{urn = Urn, name = Name, namespace = Namespace, namespace_mod = NSMod, backend_mod = Module, backend_params = ModuleParams}) ->
+ #{urn => Urn, name => Name, namespace => Namespace, namespace_mod => NSMod, backend_mod => Module, backend_params => ModuleParams}.
+
+-spec list(store()) -> {ok, dreki_store_namespace:collection()} | {error, any()}.
+list(SArg) ->
+ get_store_(SArg, list_, []).
+
+new(SArg) ->
+ get_store_(SArg, new_, []).
+
+get(SArg) ->
+ get_store_(SArg, get_, []).
+
+create(SArg, Data) ->
+ get_store_(SArg, create_, [Data]).
+
+update(SArg, Data) ->
+ get_store_(SArg, update_, [Data]).
+
+delete(SArg) ->
+ get_store_(SArg, delete_, []).
+
+list_(#{resource := #{directory := _}}, Store) ->
+ handle_result(Store, collection, callback(Store, list, []));
+list_(#{schema := _}, Store) ->
+ {todo, schema};
+list_(_, _) ->
+ not_supported.
+
+get_(#{resource := #{resource := #{id := Id}}}, Store) ->
+ handle_result(Store, single, callback(Store, get, [Id]));
+get_(#{schema := _}, Store) ->
+ {todo, get_schema};
+get_(_, _) -> not_supported.
+
+new_(#{resource := #{directory := _}}, Store) ->
+ NSMod = Store#store.namespace_mod,
+ New = NSMod:new(),
+ {ok, New}.
+
+create_(XUrn = #{directory := _}, Store, Data) ->
+ case validate(XUrn, Data) of
+ {ok, _} ->
+ handle_result(Store, single, callback(Store, create, [Data]));
+ {error, Errors} -> {error, #{code => validation_failed, status => 422, errors => jesse_error:to_json({error, Errors}, [])}}
+ end;
+create_(_, _, _) ->
+ not_supported.
+
+update_(XUrn = #{resource := _}, Store, Data) ->
+ case get(XUrn) of
+ {ok, Prev} ->
+ case validate(XUrn, Data) of
+ {ok, _} ->
+ handle_result(Store, single, callback(Store, create, [Data]));
+ {error, Errors} ->
+ {error, #{code => validation_failed, status => 422, errors => jesse_error:to_json({error, Errors}, [])}}
+ end;
+ Error -> Error
+ end;
+update_(_, _, _) ->
+ not_suppported.
+
+delete_(#{resource := _}, Store) ->
+ todo;
+delete_(_, _) ->
+ not_supported.
+
+get_store_(StoreArg, Fun, Args) when is_binary(StoreArg) ->
+ logger:debug("Expanding in get_store_ :) ~p", [StoreArg]),
+ case dreki_urn:expand(StoreArg) of
+ Error = {error, _} -> Error;
+ {ok, XUrn = #{resource := #{schemas := _}}} -> get_schema_(XUrn, Fun, Args);
+ {ok, XUrn = #{resource := #{schema := _}}} -> get_schema_(XUrn, Fun, Args);
+ {ok, XUrn} -> get_store_(XUrn, Fun, Args)
+ end;
+get_store_(XUrn = #{resource := _}, Fun, Args) ->
+ ?with_span(<<"dreki_store:get_store_">>, #{}, fun(_Ctx) ->
+ logger:debug("Getting store usually! ~p", [XUrn]),
+ case get_store(XUrn) of
+ Error = {error, _} -> Error;
+ {ok, Store} ->
+ case apply(?MODULE, Fun, [XUrn, Store | Args]) of
+ not_supported -> {error, {not_supported, Fun, maps:get(urn, XUrn)}};
+ Out -> Out
+ end
+ end
+ end).
+
+is_local(#store{xurn = #{kind := region}}) -> true;
+is_local(#store{xurn = #{kind := node, location := Location}}) -> Location =:= dreki_node:uri().
+
+% TODO: Rescue execution so we always checkin
+callback(Store = #store{}, Fun, Args) ->
+ callback(is_local(Store), Store, Fun, Args).
+
+% TODO: Rescue execution so we always checkin
+callback(false, Store = #store{xurn = #{location := Location}}, Fun, Args) ->
+ logger:debug("dreki_store:callback: rpc:~p ~p ~p", [Location, Fun, Args]),
+ dreki_node:rpc(Location, ?MODULE, callback, [Store, Fun, Args]);
+callback(true, #store{urn = Urn, backend_mod = Mod, backend_params = Params}, Fun, Args) ->
+ logger:debug("dreki_store:callback: local ~p ~p", [Fun, Args]),
+ case maps:get(Urn, persistent_term:get(?BACKENDS_PT), undefined) of
+ undefined -> dreki_error:error(store_backend_not_started, 503, <<"Store backend is not started">>);
+ Backend ->
+ {ok, B} = Mod:checkout(Backend),
+ Res = apply(Mod, Fun, [B | Args]),
+ ok = Mod:checkin(B),
+ Res
+ end.
+
+handle_result(_, _, {error, Err}) -> {error, Err};
+handle_result(Store, collection, {ok, Collection}) when is_list(Collection) ->
+ handle_result(Store, collection, Collection);
+handle_result(Store, collection, Collection) when is_list(Collection) ->
+ {ok, handle_collection_result(Collection, Store, [])};
+handle_result(Store, single, {ok, Item}) when is_map(Item) ->
+ {ok, handle_single_result(Item, Store)}.
+
+handle_collection_result([Item | Rest], Store, Acc0) ->
+ Acc = case handle_single_result(Item, Store) of
+ ignore -> Acc0;
+ {ok, I} -> [I | Acc0]
+ end,
+ handle_collection_result(Rest, Store, Acc);
+handle_collection_result([], Store, Acc) ->
+ format_collection(Acc, Store).
+
+handle_single_result(Item = #{id := LId}, Store = #store{namespace_mod = Mod}) ->
+ case Mod:format_item(Item) of
+ ok -> format_item(Item, Store);
+ {ok, M} -> format_item(M, Store);
+ Err -> Err
+ end.
+
+format_item(Item = #{id := LId}, Store = #store{urn = Urn}) ->
+ AtLinks = #{
+ self => <<Urn/binary, ":", LId>>,
+ parent => maps:get(location, Urn)
+ },
+ AllAtLinks = maps:merge(AtLinks, maps:get('@links', Item, #{})),
+ I = #{'@id' => <<Urn/binary, ":", LId>>, '@links' => AllAtLinks},
+ {ok, maps:merge(I, Item)}.
+
+format_collection(Data, Store = #store{urn = Urn}) ->
+ #{'@links' => #{self => Urn},
+ data => Data}.
+
+get_schema_(XUrn = #{resource := #{directory := #{directory := _, namespace := NS}, schemas := Schemas}}, Fun, Args) ->
+ get_schema_(XUrn#{resource => #{namespace => NS, schemas => Schemas}}, Fun, Args);
+get_schema_(XUrn = #{resource := #{directory := #{directory := _, namespace := NS}, schema := Schema}}, Fun, Args) ->
+ get_schema_(XUrn#{resource => #{namespace => NS, schema => Schema}}, Fun, Args);
+get_schema_(#{urn := Urn, resource := #{namespace := NS, schemas := #{schemas := all}}}, list_, _) ->
+ {_, NSMod, _} = namespace(NS),
+ Schemas = maps:fold(fun
+ (default, Value, Acc) -> maps:put(default, Value, Acc);
+ (SchemaName, Content, Acc) ->
+ SUrn = <<Urn/binary, ":", SchemaName/binary>>,
+logger:debug("Content is ~p", [Content]),
+ Schema = maps:fold(fun
+ (default_version, Value, CAcc) -> maps:put(default_version, Value, CAcc);
+ (Vsn, _, CAcc) when is_binary(Vsn) ->
+ Version = #{id => <<SUrn/binary, ":", Vsn/binary>>, version => Vsn},
+ maps:put(versions, [Version | maps:get(versions, CAcc, [])], CAcc)
+ end, #{id => SUrn}, Content),
+ maps:put(schemas, [Schema | maps:get(schemas, Acc, [])], Acc)
+ end, #{}, NSMod:schemas()),
+ {ok, Schemas};
+get_schema_(#{urn := Urn, resource := #{namespace := NS, schemas := #{schemas := Query}}}, list_, _) ->
+ logger:debug("Querying Schemas ~p", [Query]),
+ {_, NSMod, _} = namespace(NS),
+ case maps:get(Query, NSMod:schemas(), not_found) of
+ not_found -> {error, not_found};
+ SchemaSpec ->
+ Schema = maps:fold(fun
+ (default, Value, CAcc) -> maps:put(default_version, Value, CAcc);
+ (Vsn, _, CAcc) ->
+ Version = #{id => <<Urn/binary, ":", Vsn/binary>>, version => Vsn},
+ maps:put(versions, [Version | maps:get(versions, CAcc, [])], CAcc)
+ end, #{id => Urn}, SchemaSpec),
+ {ok, Schema}
+ end;
+get_schema_(#{urn := Urn, resource := #{namespace := NS, schema := #{schema := default}}}, get_, Args) ->
+ logger:debug("Looking for default schema!"),
+ {_, NSMod, _} = namespace(NS),
+ Schemas = NSMod:schemas(),
+ SchemaName = maps:get(default, Schemas, not_found),
+ logger:debug("=+> SchemaName ~p IN ~p", [SchemaName, Schemas]),
+ case maps:get(SchemaName, Schemas, not_found) of
+ not_found -> not_found;
+ Schema ->
+ case maps:get(default_version, Schema, not_found) of
+ not_found -> not_found;
+ SchemaVer ->
+ NewUrn = binary:replace(Urn, <<"schemas::">>, <<"schemas:", SchemaName/binary, ":", SchemaVer/binary>>),
+ {ok, XUrn} = dreki_urn:expand(NewUrn),
+ logger:debug("Looking up default schema as ~p", [NewUrn, XUrn]),
+ get_schema_(XUrn, get_, Args)
+ end
+ end;
+get_schema_(XUrn = #{resource := #{namespace := NS, schema := #{schema := SchemaName, version := Version}}}, get_, _) ->
+ {_, NSMod, _} = namespace(NS),
+ case maps:get(SchemaName, NSMod:schemas(), not_found) of
+ not_found -> not_found;
+ Schema -> format_schema(maps:get(Version, Schema, not_found), SchemaName, Version, XUrn)
+ end;
+get_schema_(XUrn = #{urn := Urn}, Method, _) ->
+ {error, {unsupported, Urn, XUrn, Method}}.
+
+format_schema(not_found, _, _, _) ->
+ {error, not_found};
+format_schema(Schema, SchemaName, SchemaVersion, XUrn = #{urn := Urn}) ->
+ {ok, maps:fold(fun (K, V, Acc) -> {K2, V2} = format_schema_field(K, V, SchemaName, SchemaVersion, XUrn), maps:put(K2, V2, Acc) end, #{}, Schema#{<<"$id">> => Urn})}.
+
+format_schema_field('$$ref', SubPath, PSN, PSV, #{urn := Urn}) ->
+ [SN, SV] = binary:split(SubPath, <<":">>),
+ NewUrn = binary:replace(Urn, <<PSN/binary, ":", PSV/binary>>, <<SN/binary, ":", SV/binary>>),
+ {'$ref', NewUrn};
+format_schema_field(Key, Map, PSn, PSv, XUrn) when is_map(Map) ->
+ {Key, maps:fold(fun (K, V, Acc) -> {K2, V2} = format_schema_field(K, V, PSn, PSv, XUrn), maps:put(K2, V2, Acc) end, #{}, Map)};
+format_schema_field(Key, List, PSn, PSv, XUrn) when is_list(List) ->
+ {Key, lists:map(fun (V) -> format_schema_field(V, PSn, PSv, XUrn) end, List)};
+format_schema_field(K, V, _, _, _) ->
+ {K, V}.
+format_schema_field(Map, PSn, PSv, XUrn) when is_map(Map) ->
+ maps:fold(fun (K, V, Acc) -> {K2, V2} = format_schema_field(K, V, PSn, PSv, XUrn), maps:put(K2, V2, Acc) end, #{}, Map);
+format_schema_field(List, PSn, PSv, XUrn) when is_list(List) ->
+ lists:map(fun (V) -> {_, Va} = format_schema_field(V, PSn, PSv, XUrn), Va end, List);
+format_schema_field(Value, _, _, _) ->
+ Value.
+
+schema_to_urn(NameAndVer, XUrn) ->
+ [Name, Ver] = binary:split(NameAndVer, <<":">>),
+ Namespace = case XUrn of
+ #{resource := #{namespace := NS}} -> NS;
+ #{resource := #{directory := #{namespace := NS}}} -> NS;
+ #{resource := #{resource := #{namespace := NS}}} -> NS
+ end,
+ dreki_urn:to_urn(XUrn#{resource => #{namespace => NS, schema => #{schema => Name, version => Ver}}}).
+
+validate(XUrn, Data) ->
+ Res = case maps:get(<<"@schema">>, Data, undefined) of
+ undefined -> get(XUrn#{resource => #{namespace => get_xurn_namespace(XUrn), schema => #{schema => default}}});
+ Urn = <<"dreki:", _/binary>> -> get(Urn);
+ SchemaNameAndVer -> get(schema_to_urn(SchemaNameAndVer, XUrn))
+ end,
+ case Res of
+ {ok, Schema} -> validate_(XUrn, Schema, Data);
+ Error -> Error
+ end.
+
+get_xurn_namespace(#{resource := #{namespace := NS}}) -> NS;
+get_xurn_namespace(#{resource := #{directory := #{namespace := NS}}}) -> NS;
+get_xurn_namespace(#{resource := #{resource := #{namespace := NS}}}) -> NS.
+
+validate_(XUrn, Schema, Data) ->
+ JesseOpts = [{allowed_errors, infinity},
+ {schema_loader_fun, fun get/1}],
+ jesse:validate_with_schema(Schema, Data, JesseOpts).
diff --git a/apps/dreki/src/dreki_store_backend.erl b/apps/dreki/src/dreki_store_backend.erl
new file mode 100644
index 0000000..55db90e
--- /dev/null
+++ b/apps/dreki/src/dreki_store_backend.erl
@@ -0,0 +1,33 @@
+-module(dreki_store_backend).
+-include("dreki.hrl").
+
+-type t() :: module().
+
+-type backend_ref() :: any().
+-type backend_checkout_ref() :: any().
+
+-type args() :: any().
+
+-callback valid_store(dreki_expanded_uri(), Namespace_mod :: module(), Args :: any()) -> ok | {error, any()}.
+
+-callback start() -> ok | {error, ba}.
+
+-callback start(binary(), binary(), dreki_urn:urn(), args()) -> {ok, backend_ref()} | {error, any()}.
+
+-callback stop() -> ok.
+
+-callback stop(backend_ref()) -> ok.
+
+-callback checkout(backend_ref()) -> {ok, backend_checkout_ref()} | {error, any()}.
+
+-callback checkin(backend_checkout_ref()) -> ok.
+
+-callback list(backend_checkout_ref()) -> {ok, dreki_store_namespace:collection()}.
+
+-callback get(backend_checkout_ref(), dreki_id()) -> {ok, dreki_store_namespace:item()} | not_found | {error, any()}.
+
+-callback create(backend_checkout_ref(), dreki_store_namespace:item()) -> ok | {error, any()}.
+
+-callback update(backend_checkout_ref(), dreki_store_namespace:item()) -> ok | not_found | {error, any()}.
+
+-callback delete(backend_checkout_ref(), dreki_id()) -> ok | not_found | {error, any()}.
diff --git a/apps/dreki/src/dreki_store_namespace.erl b/apps/dreki/src/dreki_store_namespace.erl
new file mode 100644
index 0000000..3095ef7
--- /dev/null
+++ b/apps/dreki/src/dreki_store_namespace.erl
@@ -0,0 +1,14 @@
+-module(dreki_store_namespace).
+-include("dreki.hrl").
+
+-type t() :: module().
+
+-type item() :: any().
+-type collection() :: [item()].
+-type name() :: binary().
+
+-callback start() -> ok | {error, any()}.
+-callback format_item(item()) -> ok | {ok, item()} | ignore.
+-callback valid_store(name(), Location :: dreki_urn:urn(), StoreName :: binary(), BackendModule :: module()) -> ok | {error, any()}.
+-callback version() -> non_neg_integer().
+-callback schemas() -> #{default := Id :: binary(), Id :: binary() => #{default_version := Vsn :: binary(), Vsn :: binary => Schema :: #{}}}.
diff --git a/apps/dreki/src/dreki_sup.erl b/apps/dreki/src/dreki_sup.erl
new file mode 100644
index 0000000..c1aa636
--- /dev/null
+++ b/apps/dreki/src/dreki_sup.erl
@@ -0,0 +1,39 @@
+%%%-------------------------------------------------------------------
+%% @doc dreki top level supervisor.
+%% @end
+%%%-------------------------------------------------------------------
+
+-module(dreki_sup).
+
+-behaviour(supervisor).
+
+-export([start_link/0]).
+
+-export([init/1]).
+
+-define(SERVER, ?MODULE).
+
+start_link() ->
+ supervisor:start_link({local, ?SERVER}, ?MODULE, []).
+
+%% sup_flags() = #{strategy => strategy(), % optional
+%% intensity => non_neg_integer(), % optional
+%% period => pos_integer()} % optional
+%% child_spec() = #{id => child_id(), % mandatory
+%% start => mfargs(), % mandatory
+%% restart => restart(), % optional
+%% shutdown => shutdown(), % optional
+%% type => worker(), % optional
+%% modules => modules()} % optional
+init([]) ->
+ SupFlags = #{strategy => one_for_all,
+ intensity => 10,
+ period => 5},
+ ChildSpecs = [
+ #{id => dreki_event_manager, start => {dreki_event_manager, start_link, [[]]}},
+ #{id => dreki_world_server, start => {dreki_world_server, start_link, [[]]}},
+ #{id => dreki_node_server, start => {dreki_node_server, start_link, [[]]}}
+ ],
+ {ok, {SupFlags, ChildSpecs}}.
+
+%% internal functions
diff --git a/apps/dreki/src/dreki_task.erl b/apps/dreki/src/dreki_task.erl
new file mode 100644
index 0000000..b762aea
--- /dev/null
+++ b/apps/dreki/src/dreki_task.erl
@@ -0,0 +1,58 @@
+-module(dreki_task).
+
+-include("dreki.hrl").
+
+-type new_params() :: #{handler := dreki_task_handler(),
+ id => dreki_id(),
+ description => binary(),
+ params => #{}}.
+
+-export([new/1, validate/1, to_map/1]).
+-export([id/1, description/1, handler/1, params/1]).
+-export([description/2, handler/2, params/2]).
+
+-spec new(new_params()) -> {ok, dreki_task()} | {error, Reason::term()}.
+new(Map) ->
+ Id = maps:get(id, Map, dreki_id:get()),
+ Description = maps:get(description, Map, undefined),
+ Params = maps:get(params, Map, #{}),
+ Handler = maps:get(handler, Map, undefined),
+ Task = #dreki_task{id=Id, handler=Handler, params=Params, description=Description,
+ persisted=false, dirty=true},
+ validate(Task).
+
+-spec validate(dreki_task()) -> {ok, dreki_task()} | {error, Reason::term()}.
+validate(#dreki_task{handler = undefined}) ->
+ {error, {required, handler}};
+validate(Task = #dreki_task{id = Id, handler = _Handler}) ->
+ case dreki_id:valid(Id) of
+ {error, Err} -> {error, Err};
+ ok -> {ok, Task}
+ end.
+
+id(Task = #dreki_task{}) ->
+ Task#dreki_task.id.
+
+description(Task = #dreki_task{}) ->
+ Task#dreki_task.description.
+
+handler(Task = #dreki_task{}) ->
+ Task#dreki_task.handler.
+
+params(Task = #dreki_task{}) ->
+ Task#dreki_task.params.
+
+handler(Task = #dreki_task{}, NewHandler) ->
+ Task#dreki_task{handler=NewHandler, dirty=true}.
+
+description(Task = #dreki_task{}, NewDescription) ->
+ Task#dreki_task{description=NewDescription, dirty=true}.
+
+params(Task = #dreki_task{}, NewParams) ->
+ Task#dreki_task{params=NewParams, dirty=true}.
+
+to_map(Task = #dreki_task{id = Id, handler = Handler, description = Description, params = Params}) ->
+ #{id => Id,
+ handler => Handler,
+ description => Description,
+ params => Params}.
diff --git a/apps/dreki/src/dreki_tasks.erl b/apps/dreki/src/dreki_tasks.erl
new file mode 100644
index 0000000..0899386
--- /dev/null
+++ b/apps/dreki/src/dreki_tasks.erl
@@ -0,0 +1,141 @@
+-module(dreki_tasks).
+-include("dreki.hrl").
+
+-behaviour(dreki_store_namespace).
+-export([start/0, version/0, valid_store/4, format_item/1, schemas/0, new/0]).
+
+%% old stuff
+-export([resolve/1, exists/1, read_uri/2]).
+
+start() -> ok.
+
+valid_store(_Namespace, _Location, _Name, _BackendMod) -> ok.
+
+format_item(Item) -> ok.
+
+handlers() -> [dreki_tasks_script, dreki_tasks_cloyster].
+
+-record(?MODULE, {
+ id,
+ version,
+ schema,
+ handler,
+ handler_manifest
+}).
+
+version() -> 1.
+
+new() ->
+ #{
+ <<"@schema">> => <<"task:1.0">>,
+ <<"id">> => ksuid:gen_id(),
+ <<"handler">> => <<"dreki_tasks_cloyster">>,
+ <<"handler_manifest">> => #{
+ <<"@schema">> => <<"cloyster-task:1.0">>,
+ <<"script">> => <<>>
+ }
+ }.
+
+schemas() ->
+ Subs = lists:foldr(fun (Handler, Acc) -> maps:merge(Acc, Handler:schemas()) end,
+ #{}, handlers()),
+ maps:merge(Subs, #{
+ default => <<"task">>,
+ <<"task">> => schemas(task)
+ }).
+
+schemas(task) ->
+ #{
+ default_version => <<"1.0">>,
+ <<"1.0">> => schemas(task, <<"1.0">>)
+ }.
+
+schemas(task, <<"1.0">>) ->
+ Handlers = handlers(),
+ Manifests0 = lists:map(fun (Handler) -> {Handler, Handler:schema_field(handler_manifest)} end, Handlers),
+ Manifests = lists:foldr(fun
+ ({Handler, undefined}, Acc) -> Acc;
+ ({Handler, Schema}, Acc) ->
+ Schemas = maps:fold(fun
+ (Atom, _, Acc) when is_atom(Atom) -> Acc;
+ (Vsn, _, Acc) -> [#{'$$ref' => <<Schema/binary, ":", Vsn/binary>>} | Acc]
+ end, [], maps:get(Schema, Handler:schemas())),
+ Acc ++ Schemas
+ end, [], Manifests0),
+
+ #{
+ version => 'draft-06',
+ title => <<"Task">>,
+ type => object,
+ properties => #{
+ id => #{type => string, <<"dreki:form">> => #{default => {ksuid, gen_id, []}}},
+ schema => #{type => string},
+ name => #{type => string, title => <<"Name">>},
+ handler => #{type => string, title => <<"Handler">>, enum => handlers()},
+ handler_manifest => #{'$ref' => <<"handler_manifest">>}
+ },
+ '$defs' => #{
+ <<"handler_manifest">> => #{anyOf => Manifests}
+ },
+ required => [handler, handler_manifest]
+ }.
+
+%% old stuff
+
+read_uri(undefined, Uri) ->
+ {ok, #{stores => #{}}};
+read_uri(Path, Uri) ->
+ case binary:split(Path, <<":">>, [global]) of
+ [<<>>, <<>>] -> {error, invalid_resource};
+ [<<>>, Id] -> {ok, #{resolve => #{kind => tasks, id => Id}}};
+ [S] -> {ok, #{store => #{kind => tasks, id => S}}};
+ [S, <<>>] -> {ok, #{store => #{kind => tasks, id => S}}};
+ [S, Id] -> {ok, #{resource => #{kind => tasks, store => S, id => Id}}};
+ R -> {error, {invalid_address, {Path, R}}}
+ end.
+
+all(Uri) when is_binary(Uri) ->
+ all(dreki_world:read_uri(Uri));
+all({ok, Uri = #{kind := tasks, uri := Uri, resource := Res = #{store := #{id := _S}}}}) ->
+ {ok, Mod, Store} = dreki_node:get_store(Uri),
+ Mod:all(Store);
+all({ok, Uri}) -> {error, unresolvable_uri, Uri};
+all(Error = {error, _}) -> Error.
+
+
+resolve(Id) ->
+ Path = dreki_world:path(),
+ case binary:match(Id, <<Path/binary, "::tasks:">>) of
+ {0,End} ->
+ StoreAndId = binary:part(Id, {End, byte_size(Id) - End}),
+ [StoreN, LId] = binary:split(StoreAndId, <<":">>),
+ {ok, {node(), StoreN, LId}}
+ end.
+
+exists({Node, StoreN, LId}) when Node =:= node() ->
+ #{mod := Mod, args := Args} = maps:get(StoreN, load_local_stores()),
+ {ok, Db} = Mod:open(Args),
+ Mod:exists(Db, LId);
+exists(N = {Node, _, _}) ->
+ erpc:call(Node, dreki_tasks, exists, [N]);
+exists(Id) when is_binary(Id) ->
+ case resolve(Id) of
+ {ok, N} -> exists(N);
+ Error = {error, _} -> Error
+ end.
+
+load_local_stores() ->
+ {ok, Val} = application:get_env(dreki, local_tasks_stores),
+ _World = #{path := Path, me := Me} = dreki_world:to_map(),
+ MapFn = fun ({Name, Mod, Args, Env}, Acc) ->
+ Store = #{local => true,
+ node => Me,
+ path => <<Path/binary, "::tasks:", Name/binary>>,
+ mod => Mod,
+ args => Args,
+ env => Env,
+ name => Name
+ },
+ maps:put(Name, Store, Acc)
+ end,
+ lists:foldr(MapFn, #{}, Val).
diff --git a/apps/dreki/src/dreki_tasks_cloyster.erl b/apps/dreki/src/dreki_tasks_cloyster.erl
new file mode 100644
index 0000000..3fb045d
--- /dev/null
+++ b/apps/dreki/src/dreki_tasks_cloyster.erl
@@ -0,0 +1,21 @@
+-module(dreki_tasks_cloyster).
+-export([schemas/0, schema_field/1]).
+
+schema_field(handler_manifest) -> <<"cloyster-task">>.
+
+schemas() ->
+ #{
+ <<"cloyster-task">> => #{
+ default_version => <<"1.0">>,
+ <<"1.0">> => #{
+ version => 'draft-06',
+ title => <<"Cloyster Task Definition">>,
+ type => object,
+ properties => #{
+ <<"script">> => #{type => string}
+ },
+ required => [script]
+ }
+ }
+ }.
+
diff --git a/apps/dreki/src/dreki_tasks_script.erl b/apps/dreki/src/dreki_tasks_script.erl
new file mode 100644
index 0000000..8eeb563
--- /dev/null
+++ b/apps/dreki/src/dreki_tasks_script.erl
@@ -0,0 +1,24 @@
+-module(dreki_tasks_script).
+-export([schemas/0, schema_field/1]).
+
+schema_field(handler_manifest) -> <<"script-task">>.
+
+schemas() ->
+ #{
+ <<"script-task">> => #{
+ default_version => <<"1.0">>,
+ <<"1.0">> => #{
+ version => 'draft-06',
+ title => <<"Executable Script Task">>,
+ type => object,
+ properties => #{
+ <<"id">> => #{type => string, <<"dreki:form">> => #{default => generate_id}},
+ <<"name">> => #{type => string},
+ <<"description">> => #{type => string, <<"dreki:form">> => #{input => textarea, textarea_mode => markdown}},
+ <<"executable">> => #{type => string, default => <<"/bin/sh">>},
+ <<"script">> => #{type => string, <<"dreki:form">> => #{input => textarea}}
+ },
+ required => [script]
+ }
+ }
+ }.
diff --git a/apps/dreki/src/dreki_uri.erl b/apps/dreki/src/dreki_uri.erl
new file mode 100644
index 0000000..e912d47
--- /dev/null
+++ b/apps/dreki/src/dreki_uri.erl
@@ -0,0 +1 @@
+-module(dreki_uri).
diff --git a/apps/dreki/src/dreki_urn.erl b/apps/dreki/src/dreki_urn.erl
new file mode 100644
index 0000000..2da6c69
--- /dev/null
+++ b/apps/dreki/src/dreki_urn.erl
@@ -0,0 +1,173 @@
+-module(dreki_urn).
+-define(PT, dreki_urn).
+
+-export([nid/0, start/0, namespaces/0, namespace/1, register_namespace/3, expand/1, to_urn/1]).
+
+-callback expand_urn_resource_rest(namespace(), expanded_resource(), resource_part(), NsEnv :: #{}) -> {ok, expanded_resource()} | {error, any()}.
+
+-type urn() :: binary().
+-type namespace() :: binary().
+-type directory() :: binary().
+-type location_part() :: binary().
+-type resource_part() :: binary().
+
+nid() -> <<"dreki">>.
+
+-spec start() -> ok.
+start() ->
+ persistent_term:put(?PT, #{}).
+
+-spec namespaces() -> #{namespace() := {module(), #{}}}.
+namespaces() ->
+ persistent_term:get(?PT).
+
+-spec namespace(namespace()) -> {module(), #{}} | undefined.
+namespace(NS) ->
+ maps:get(NS, namespaces(), undefined).
+
+-spec register_namespace(namespace(), module(), #{}) -> ok | {error, {namespace_already_registered, namespace(), module()}}.
+register_namespace(Namespace, Mod, Env) ->
+ Map = persistent_term:get(?PT),
+ case maps:get(Namespace, Map, {Mod, undefined}) of
+ {Mod, Env} -> ok;
+ {Mod, _} -> persistent_term:put(?PT, maps:put(Namespace, {Mod, Env}, Map));
+ {OtherMod, _} -> {error, {namespace_already_registered, Namespace, OtherMod}}
+ end.
+
+-type expand_error() :: not_dreki_urn.
+
+-spec expand(urn()) -> {ok, expanded()} | {error, expand_error()}.
+expand(U = <<"dreki:", _/binary>>) ->
+ expand(U, dreki_world:root_path());
+expand(U = <<"urn:dreki:", _/binary>>) ->
+ expand(U, dreki_world:root_path());
+expand(_Invalid) ->
+ {error, not_dreki_urn}.
+%% Remove root
+%%expand(U, Root) ->
+%% expand(U, Root, Rest).
+%%expand(U, Root) ->
+%% {error, #{message => <<"URI outside of domain">>, uri => U, domain => Root}}.
+%% Extract hierarchy
+expand(U, Root) ->
+ {Hier, Res} = case binary:split(U, <<"::">>) of
+ [H, R] -> {H, R};
+ [H] -> {H, undefined}
+ end,
+ % XXX: Do we really need to store theses paths ? o_o
+ case dreki_world:get_path(Hier) of
+ {ok, Thing} -> expand(U, Root, Hier, Res, Thing);
+ Error -> Error
+ end.
+
+-type expanded() :: #{
+ urn := urn(),
+ world := binary(),
+ location := location_part(),
+ kind := region | node | [],
+ resource => expanded_resource(),
+ query => #{},
+ fragment => binary()
+}.
+
+-type expanded_resource() :: #{
+ resource => expanded_resource_resource(),
+ directory => expanded_resource_directory(),
+ namespace => namespace(),
+ lookup => expanded_resource_lookup()
+}.
+
+-type expanded_resource_lookup() :: #{
+ namespace => namespace(),
+ id => binary()
+}.
+
+-type expanded_resource_resource() :: #{
+ namespace => namespace(),
+ directory => binary(),
+ id => binary()
+}.
+
+-type expanded_resource_directory() :: #{
+ namespace => namespace(),
+ directory => directory()
+}.
+
+expand(Urn, Root, Path, ResPart, Thing) ->
+ Kind = case maps:keys(Thing) of
+ [K] -> K;
+ Ks -> Ks
+ end,
+ XUrn = #{world => Root, kind => Kind, domain => Root, location => Path, urn => Urn},
+ case expand_resource(ResPart) of
+ {ok, Resource} ->
+ {ok, XUrn#{resource => Resource}};
+ {error, invalid_resource} ->
+ {error, #{message => <<"Invalid resource">>, uri => Urn, resource => ResPart}};
+ {error, invalid_namespace, NS} ->
+ {error, #{message => <<"Invalid namespace">>, uri => Urn, namespace => NS}}
+ end.
+
+expand_resource(undefined) ->
+ {ok, undefined};
+expand_resource(Path) ->
+ {NS, Rest} = case binary:split(Path, <<":">>) of
+ [N, R] -> {N, R};
+ [N] -> {N, <<>>}
+ end,
+ case namespace(NS) of
+ NSS = {_, _} -> expand_resource(NS, Rest, NSS);
+ undefined -> {error, invalid_namespace, NS}
+ end.
+
+expand_resource(NS, FullResPart, {NSMod, NSEnv}) ->
+ {ResPart, Rest} = case binary:split(FullResPart, <<"::">>) of
+ [R] -> {R, undefined};
+ [R, Rs] -> {R, Rs}
+ end,
+ case expand_resource_part(NS, ResPart) of
+ Error = {error, _} -> Error;
+ {ok, Res} -> expand_resource_rest(NS, NSMod, NSEnv, Res, Rest)
+ end.
+
+expand_resource_part(NS, ResPart) ->
+ case binary:split(ResPart, <<":">>, [global]) of
+ [<<>>, <<>>] -> {error, invalid_resource};
+ [<<>>, Id] -> {ok, #{lookup => #{namespace => NS, id => Id}}};
+ [<<>>] -> {ok, #{namespace => NS}};
+ [S] -> {ok, #{directory => #{namespace => NS, directory => S}}};
+ [S, <<>>] -> {ok, #{directory => #{namespace => NS, directory => S}}};
+ [S, Id] -> {ok, #{resource => #{namespace => NS, directory => S, id => Id}}};
+ [] -> {ok, #{namespace => NS}};
+ R -> {error, {invalid_address, {ResPart, R}}}
+ end.
+
+expand_resource_rest(_, _, _, Resource, undefined) ->
+ {ok, Resource};
+expand_resource_rest(NS, NSMod, NSEnv, Resource, Part) ->
+ case NSMod:expand_urn_resource_rest(NS, Resource, Part, NSEnv) of
+ {ok, Data} -> {ok, Data};
+ error -> {error, {invalid_part, {NS, Part}}}
+ end.
+
+to_urn(XUrn = #{location := Location}) ->
+ case resource_to_urn_part(XUrn) of
+ Binary when is_binary(Binary) -> <<Location/binary, "::", Binary/binary>>;
+ undefined -> Location
+ end.
+
+resource_to_urn_part(#{resource := Res0 = #{schemas := all}}) ->
+ Res = maps:remove(schemas, Res0),
+ ResP = resource_to_urn_part(Res),
+ <<ResP/binary, "::", "schemas">>;
+resource_to_urn_part(#{resource := Res0 = #{schema := #{schema := Schema, version := Vers}}}) ->
+ Res = maps:remove(schema, Res0),
+ ResP = resource_to_urn_part(Res),
+ <<ResP/binary, "::", "schemas:", Schema/binary, ":", Vers/binary>>;
+resource_to_urn_part(#{resource := #{namespace := NS}}) ->
+ NS;
+resource_to_urn_part(#{resource := #{directory := #{directory := Dir, namespace := NS}}}) ->
+ <<NS/binary, ":", Dir/binary>>;
+resource_to_urn_part(#{resource := #{resource := #{id := Id, directory := Dir, namespace := NS}}}) ->
+ <<NS/binary, ":", Dir/binary, ":", Id/binary>>.
+
diff --git a/apps/dreki/src/dreki_world.erl b/apps/dreki/src/dreki_world.erl
new file mode 100644
index 0000000..437b6c8
--- /dev/null
+++ b/apps/dreki/src/dreki_world.erl
@@ -0,0 +1,366 @@
+-module(dreki_world).
+
+-export([ensure_consistency/0, index/2]).
+-export([get_path/1, get_path/2, paths/0, put_path/3]).
+-export([get_region_from_dns_name/1, get_region/1, get_region/2, get_region/3, create_region/1, put_region/3]).
+-export([node/0, nodes/0, get_node_from_dns_name/1, get_node/1, create_node_from_dns_name/2, create_node/2, put_node/3]).
+-export([refresh_index/2, get/2, get/3, maybe_put_index/5]).
+-export([namespace_to_module/1]).
+-export([path_to_domain/1]).
+-export([to_map/0, root_domain/0, internal_domain/0, domain/0, hierarchy/0, parent/0, me/0, path/0, root_path/0, strip_root_path/1, put_root_path/1, read_uri/1, domain_to_path/1]).
+
+ensure_consistency() ->
+ Dns = dreki_world_dns:as_map(),
+ Vertices = maps:get(vertices, Dns),
+ ensure_consistency(Vertices, []).
+
+ensure_consistency([#{type := root, name := Root} | Rest], Acc) ->
+ Acc = case get_region_from_dns_name(Root) of
+ {error, {not_found, _}} ->
+ ok = create_region_from_dns_name(Root),
+ put_region(Root, root, true),
+ [{root, Root} | Acc];
+ _ ->
+ Acc
+ end,
+ Res = [Log || Kind <- [tasks], Log = {create, _} <- [ensure_global_root_stores(Root, Kind)]],
+ ensure_consistency(Rest, Res ++ Acc);
+
+ensure_consistency([#{type := region, name := Region} | Rest], Acc) ->
+ case get_region_from_dns_name(Region) of
+ {error, {not_found, _}} ->
+ ensure_consistency(Rest, [{Region, create_region_from_dns_name(Region)} | Acc]);
+ _ ->
+ ensure_consistency(Rest, Acc)
+ end;
+ensure_consistency([_ | Rest], Acc) ->
+ ensure_consistency(Rest, Acc);
+ensure_consistency([], Acc) ->
+ Acc.
+
+ensure_global_root_stores(Root, Kind) ->
+ {ok, Region} = get_region_from_dns_name(Root),
+ RegionUri = maps:get(uri, Region),
+ KindB = atom_to_binary(Kind),
+ StoreUri = <<RegionUri/binary, "::", KindB/binary, ":global">>,
+ case dreki_store:get_store(StoreUri) of
+ {ok, _store} -> ok;
+ _ -> {create, {Root,Kind,dreki_store:create_store(StoreUri, dreki_world_store, #{}, #{})}}
+ end.
+
+put_index(IdxKey, IdxValue, Uri, Data) ->
+ plum_db:put({IdxKey, IdxValue}, Uri, Data).
+
+maybe_put_index(Kind, IdxKey, IdxValue, Uri, Data) ->
+ case lists:member(IdxKey, index_fields(Kind)) of
+ true -> put_index(index_key(IdxKey), IdxValue, Uri, Data);
+ false -> ok
+ end.
+
+index_key(roles) -> 'idx:roles';
+index_key(tags) -> 'idx:tags'.
+
+index(Key, Value) ->
+ Idx = index_key(Key),
+ plum_db:fold(fun
+ ({_, ['$deleted']}, Acc) -> Acc;
+ ({Key, Val}, Acc) ->
+ [{Key, Val} | Acc]
+ end, [], {Idx, Value}).
+
+index_fields(node) -> [roles, tags];
+index_fields(region) -> [roles, tags];
+index_fields(_) -> [roles, tags].
+
+refresh_index(Kind, Map) when is_atom(Kind) ->
+ refresh_index(index_fields(Kind), Kind, Map).
+refresh_index([Field | Rest], Kind, Map = #{uri := URI}) ->
+ Values = case maps:get(Field, Map, undefined) of
+ undefined -> [];
+ V when is_list(V) -> V;
+ V -> [V]
+ end,
+ [maybe_put_index(Kind, Field, V, URI, undefined) || V <- Values],
+ refresh_index(Rest, Kind, Map);
+refresh_index([], _, _) ->
+ ok.
+
+clean_path(Path) ->
+ Len = byte_size(Path) - 1,
+ case Path of
+ <<P:Len/binary, ":">> -> P;
+ P -> P
+ end.
+
+put_path(Path0, Kind, Uri) ->
+ Path = clean_path(Path0),
+ {ok, Links} = get({paths, Path}, Path, [{default, #{}}]),
+ case maps:find(Kind, Links) of
+ {ok, _} -> {error, {exists, {Path, Kind}}};
+ error ->
+ ok = plum_db:put({paths, Path}, Path, Links#{Kind => Uri}),
+ ok
+ end.
+
+get_path(Path0) ->
+ Path = clean_path(Path0),
+ case get({paths, Path}, Path) of
+ not_found -> {error, {not_found, {path, Path}}};
+ {ok, Data} -> {ok, Data}
+ end.
+
+get_path(Path, Kind) ->
+ case get_path(Path) of
+ {ok, Map} ->
+ case maps:get(Kind, Map, undefined) of
+ undefined -> {error, {not_found, {Path, Kind}}};
+ Value -> {ok, Value}
+ end;
+ Error = {error, _} -> Error
+ end.
+
+paths() ->
+ plum_db:fold(fun
+ ({_, ['$deleted']}, Acc) -> Acc;
+ ({Path, Value}, Acc) ->
+ maps:put(Path, Value, Acc)
+ end, #{}, {paths, '_'}).
+
+get(Prefix, Key) ->
+ get(Prefix, Key, []).
+
+get(Prefix, Key, Opts) ->
+ case plum_db:get(Prefix, Key, Opts) of
+ undefined -> not_found;
+ ['$deleted'] -> not_found;
+ Val -> {ok, Val}
+ end.
+
+region_uri_from_dns_name(Name) ->
+ Root = internal_domain(),
+ if
+ Root =:= Name -> root_path();
+ true -> domain_to_path(Name)
+ end.
+
+get_region_from_dns_name(Name) ->
+ get_region(region_uri_from_dns_name(Name)).
+
+get_region(Path) ->
+ case get_path(Path, region) of
+ {ok, _} ->
+ Region = plum_db:fold(fun
+ ({_, ['$deleted']}, M) -> M;
+ ({{_, K}, V}, M) -> maps:put(K, V, M)
+ end, #{uri => Path}, {regions, Path}),
+ {ok, Region};
+ Error -> Error
+ end.
+
+get_region(Path, Key) ->
+ get_region(Path, Key, undefined).
+
+get_region(Path, Key, GetOpts) ->
+ case get_path(Path, region) of
+ {ok, _} -> get({regions, Path}, {Path, Key}, GetOpts);
+ Error -> Error
+ end.
+
+create_region_from_dns_name(Name) ->
+ create_region(region_uri_from_dns_name(Name)).
+
+create_region(Path) ->
+ case get_region(Path) of
+ {ok, _} -> {error, {exists, {region, Path}}};
+ {error, {not_found, _}} ->
+ ok = put_path(Path, region, Path),
+ ok = put_region(Path, region, Path),
+ {ok, R} = get_region(Path),
+ ok = refresh_index(region, R),
+ ok
+ end.
+
+put_region(Path, Key, Value) ->
+ case get_path(Path, region) of
+ {ok, _} ->
+ ok = plum_db:put({regions, Path}, {Path, Key}, Value),
+ ok = maybe_put_index(region, Key, Value, Path, undefined);
+ Error -> Error
+ end.
+
+nodes() ->
+ lists:foldr(fun (Domain, Acc) ->
+ case get_node_from_dns_name(Domain) of
+ {ok, Node} -> [Node | Acc];
+ _ -> Acc
+ end
+ end, [], dreki_world_dns:nodes()).
+
+node() ->
+ dreki_world:domain_to_path(dreki_world:domain()).
+
+get_node_from_dns_name(Name) ->
+ get_node(domain_to_path(Name)).
+
+get_node(Path) ->
+ case get_path(Path, node) of
+ {ok, _} ->
+ Node = plum_db:fold(fun
+ ({_, ['$deleted']}, M) -> M;
+ ({{_, K}, [V]}, M) -> maps:put(K, V, M)
+ end, #{uri => Path}, {nodes, Path}),
+ {ok, Node};
+ Error -> Error
+ end.
+
+create_node_from_dns_name(Name, Params) ->
+ create_node(domain_to_path(Name), Params).
+
+create_node(Path, Params) ->
+ case get_path(Path, node) of
+ {ok, _} -> {error, {exists, {node, Path}}};
+ {error, {not_found, _}} ->
+ ok = put_path(Path, node, Path),
+ ok = put_node(Path, node, Path),
+ [ok = put_node(Path, Key, Value) || {Key, Value} <- maps:to_list(Params)],
+ ok
+ end.
+
+put_node(Path, Key, Value) ->
+ case get_path(Path, node) of
+ {ok, _} -> plum_db:put({nodes, Path}, {Path, Key}, Value);
+ Error -> Error
+ end.
+
+domain_to_path(Domain) ->
+ Clean = strip_domain(Domain, internal_domain()),
+ Parts = binary:split(Clean, <<".">>, [global]),
+ Components = [<<"dreki">>, root_domain(), internal_subdomain()] ++ lists:reverse(Parts),
+ join(<<":">>, Components).
+
+path_to_domain(<<"dreki:", Rest/binary>>) ->
+ [Location | _] = binary:split(Rest, <<"::">>),
+ Components = binary:split(Location, <<":">>, [global]),
+ join(<<".">>, lists:reverse(Components)).
+
+root_domain() ->
+ get_env(root_domain).
+
+internal_domain() ->
+ get_env(internal_domain).
+
+domain() ->
+ get_env(domain).
+
+read_uri(U = <<"dreki:", _/binary>>) ->
+ read_uri(U, root_path());
+read_uri(Invalid) ->
+ {error, {bad_type, Invalid}}.
+%% Remove root
+%%read_uri(U, Root) ->
+%% read_uri(U, Root, Rest).
+%%read_uri(U, Root) ->
+%% {error, #{message => <<"URI outside of domain">>, uri => U, domain => Root}}.
+%% Extract hierarchy
+read_uri(U, Root) ->
+ {Hier, Res} = case binary:split(U, <<"::">>) of
+ [H, R] -> {H, R};
+ [H] -> {H, undefined}
+ end,
+ case get_path(H) of
+ {ok, Thing} -> read_uri_(U, Root, H, Res, Thing);
+ Error -> Error
+ end.
+
+read_uri_(U, Root, Path, ResPath, Thing) ->
+ Kind = case maps:keys(Thing) of
+ [K] -> K;
+ Ks -> Ks
+ end,
+ RegionStoreHint = case {maps:get(region, Thing, false), maps:get(node, Thing, false)} of
+ {false, _} -> global;
+ {_, false} -> global;
+ {_, _} -> local
+ end,
+ StoreHints = lists:foldr(fun
+ (node, Acc) -> maps:put(node, local, Acc);
+ (region, Acc) -> maps:put(region, RegionStoreHint, Acc)
+ end, #{}, maps:keys(Thing)),
+ Uri = #{domain => Root, path => Path, kind => Kind, store_hints => StoreHints, uri => U},
+ case read_resource_uri(ResPath, Uri) of
+ {ok, Resource} ->
+ {ok, Uri#{resource => Resource}};
+ {error, invalid_resource} ->
+ {error, #{message => <<"Invalid resource">>, uri => U, resource => ResPath}};
+ {error, invalid_namespace, NS} ->
+ {error, #{message => <<"Invalid namespace">>, uri => U, namespace => NS}}
+ end.
+
+read_resource_uri(undefined, _) ->
+ {ok, undefined};
+read_resource_uri(Path, Uri) ->
+ {NS, Rest} = case binary:split(Path, <<":">>) of
+ [NS, R] -> {NS, R};
+ [NS] -> {NS, undefined}
+ end,
+ case namespace_to_module(NS) of
+ {ok, Mod} -> Mod:read_uri(Rest, Uri);
+ error -> {error, invalid_namespace, NS}
+ end.
+
+namespace_to_module(<<"tasks">>) -> namespace_to_module(tasks);
+namespace_to_module(<<"names">>) -> namespace_to_module(names);
+namespace_to_module(tasks) -> {ok, dreki_tasks};
+namespace_to_module(names) -> {ok, dreki_names};
+namespace_to_module(_) -> error.
+
+internal_subdomain() ->
+ strip_domain(internal_domain(), root_domain()).
+
+root_path() ->
+ join(<<":">>, [<<"dreki">>, root_domain(), internal_subdomain()]).
+
+strip_root_path(Path) ->
+ Root = root_path(),
+ binary:replace(Path, <<Root/binary>>, <<"">>).
+
+put_root_path(Path) ->
+ join(<<":">>, [root_path(), Path]).
+
+path() ->
+ HierJ = lists:reverse(hierarchy()),
+ Components = [<<"dreki">>, root_domain(), internal_subdomain()] ++ HierJ,
+ join(<<":">>, Components).
+
+hierarchy() ->
+ binary:split(strip_domain(domain(), internal_domain()), <<".">>, [global]).
+
+parent() ->
+ case hierarchy() of
+ [_, Parent | _] -> Parent;
+ [Parent] -> Parent
+ end.
+
+me() ->
+ [Me | _] = hierarchy(),
+ Me.
+
+get_env(Key) ->
+ {ok, Val} = application:get_env(dreki, Key),
+ Val.
+
+to_map() ->
+ #{domain => #{root => root_domain(), internal => internal_domain(), self => domain()},
+ path => path(),
+ root_path => root_path(),
+ hiearchy => lists:reverse(hierarchy()),
+ parent => parent(),
+ me => me()}.
+
+strip_domain(FQDN, Parent) ->
+ binary:replace(FQDN, <<".", Parent/binary>>, <<"">>).
+
+join(_Separator, []) ->
+ <<>>;
+join(Separator, [H|T]) ->
+ lists:foldl(fun (Value, Acc) -> <<Acc/binary, Separator/binary, Value/binary>> end, H, T).
diff --git a/apps/dreki/src/dreki_world_dns.erl b/apps/dreki/src/dreki_world_dns.erl
new file mode 100644
index 0000000..058c9db
--- /dev/null
+++ b/apps/dreki/src/dreki_world_dns.erl
@@ -0,0 +1,162 @@
+-module(dreki_world_dns).
+
+-export([start/0, graph/0, node_ips/1, node/0, parents/1, nodes/0, regions/0, vertex/1, node_params/1, node_param/2, as_map/0]).
+-export([get_path/2, in_neighbours/1, out_neighbours/1]).
+
+get_path(V1, V2) ->
+ digraph:get_path(graph(), V1, V2).
+
+in_neighbours(V) ->
+ digraph:in_neighbours(graph(), V).
+
+out_neighbours(V) ->
+ digraph:out_neighbours(graph(), V).
+
+as_map() ->
+ Vertices = lists:foldr(fun (V = {Type, Key}, Acc) ->
+ {_, Label} = digraph:vertex(graph(), V),
+ BType = atom_to_binary(Type),
+ Node = <<BType/binary, ":", Key/binary>>,
+ [#{node => Node, type => Type, name => Key, data => Label} | Acc]
+ end, [], digraph:vertices(graph())),
+ Edges = lists:foldr(fun (E, Acc) ->
+ {_, {Ft, Fn}, {Tt, Tn}, Label} = digraph:edge(graph(), E),
+ BFt = atom_to_binary(Ft),
+ BTt = atom_to_binary(Tt),
+ Fk = <<BFt/binary, ":", Fn/binary>>,
+ Tk = <<BTt/binary, ":", Tn/binary>>,
+ [#{from => Fk, to => Tk, data => Label} | Acc]
+ end, [], digraph:edges(graph())),
+ #{vertices => Vertices, edges => Edges}.
+
+vertex(V) ->
+ digraph:vertex(graph(), V).
+
+nodes() ->
+ [N || V = {node, N} <- digraph_utils:topsort(graph())].
+
+regions() ->
+ vertices(region).
+
+vertices(Type) ->
+ [V || V = {Type, _} <- digraph_utils:topsort(graph())].
+
+node() ->
+ {node, dreki_world:domain()}.
+
+node_params(N) ->
+ case digraph:vertex(graph(), {node, N}) of
+ {_, Params} -> {ok, Params};
+ _ -> {error, no_such_node}
+ end.
+
+node_param(N, Key) ->
+ case node_params(N) of
+ {ok, P} -> {ok, maps:get(Key, P)};
+ Err -> Err
+ end.
+
+parents(V) ->
+ digraph:in_neighbours(graph(), V).
+
+node_ips(N) ->
+ case digraph:vertex(graph(), {node, N}) of
+ {{node, N}, #{srvs := SRVs}} ->
+ IPs = [ {I,Port} || #{name := Name, port := Port} <- SRVs,
+ T <- [a, aaaa],
+ {ok, {_,_,_,_,_,Ip}} <- [inet_res:getbyname(binary_to_list(Name), T)],
+ I <- Ip],
+ {ok, lists:flatten(IPs)};
+ Err ->
+ {error, {no_such_node, N, Err}}
+ end.
+
+start() ->
+ {ok, Graph, Errs} = build(),
+ persistent_term:put({?MODULE, graph}, Graph),
+ {ok, Errs}.
+
+graph() ->
+ persistent_term:get({?MODULE, graph}).
+
+build() ->
+ Root = dreki_world:internal_domain(),
+ Host = sd_dns(Root),
+ case inet_res:getbyname(Host, srv) of
+ {ok, {hostent, _Host, [], srv, _, SRVs}} ->
+ Graph = digraph:new([acyclic]),
+ {Name, NameErrs} = read_txt(name_dns(Root), Root),
+ V = {root, Root},
+ digraph:add_vertex(Graph, V, #{display_name => Name}),
+ Errs = collect_sd_srvs(SRVs, V, Graph, NameErrs),
+ {ok, Graph, lists:flatten(Errs)};
+ {error, DNSErr} when is_atom(DNSErr) ->
+ {error, #{error => "world_dns_error", dns_error => DNSErr, host => Host}}
+ end.
+
+expand_sd_srv(Host, Parent, Graph) ->
+ NodeHost = node_dns(Host),
+ {Vn, Acc0} = case inet_res:getbyname(NodeHost, srv) of
+ {ok, {hostent, _, _, srv, _, NSRVs}} ->
+ Targets = lists:foldr(fun ({Priority, Weight, Port, Name}, Acc) ->
+ [#{name => list_to_binary(Name), port => Port, priority => Priority, weight => Weight} | Acc]
+ end, [], NSRVs),
+ {NName, NameErrs} = read_txt(name_dns(Host), name(Host)),
+ {NodeName, NameErrs2} = read_txt(node_name_dns(Host), <<"dreki@", Host/binary>>),
+ Nv = {node, Host},
+ digraph:add_vertex(Graph, Nv, #{display_name => NName, srvs => Targets, node_name => binary_to_atom(NodeName)}),
+ digraph:add_edge(Graph, Parent, Nv, #{}),
+ {Nv, [] ++ NameErrs ++ NameErrs2};
+ {error, nxdomain} -> {undefined, []};
+ {error, VDNSErr} when is_atom(VDNSErr) ->
+ logger:log(error, #{dns_error => VDNSErr, host => NodeHost}),
+ {undefined, [{error, #{error => "world_dns_error", dns_error => VDNSErr, host => NodeHost}}]}
+ end,
+ SdHost = sd_dns(Host),
+ Acc1 = case inet_res:getbyname(SdHost, srv) of
+ {ok, {hostent, _SdHost, [], srv, _, SSRVs}} ->
+ {RName, RNameErrs} = read_txt(name_dns(Host), name(Host)),
+ V = {region, Host},
+ digraph:add_vertex(Graph, V, #{display_name => RName}),
+ case Vn of
+ undefined -> digraph:add_edge(Graph, Parent, V, #{});
+ Vn -> digraph:add_edge(Graph, Vn, V, #{})
+ end,
+ collect_sd_srvs(SSRVs, V, Graph, RNameErrs);
+ {error, nxdomain} -> [];
+ {error, DNSErr} when is_atom(DNSErr) ->
+ logger:log(error, #{dns_error => DNSErr, host => SdHost}),
+ [{error, #{error => "world_dns_error", dns_error => DNSErr, host => SdHost}}]
+ end,
+ [Acc0, Acc1].
+
+collect_sd_srvs([], _, _Graph, Acc) -> Acc;
+collect_sd_srvs([{0, 0, 1337, Entry} | Rest], Parent, Graph, Acc) ->
+ collect_sd_srvs(Rest, Parent, Graph, [expand_sd_srv(list_to_binary(Entry), Parent, Graph) | Acc]).
+
+read_txt(Host, Default) ->
+ case inet_res:getbyname(Host, txt) of
+ {error, nxdomain} -> {Default, []};
+ {ok,{hostent, _, _, _, _, Lines}} -> {list_to_binary(Lines), []};
+ {error, DNSErr} when is_atom(DNSErr) ->
+ logger:log(error, #{dns_error => DNSErr, host => Host}),
+ {Default, [#{error => "world_dns_error", dns_error => DNSErr, host => Host}]}
+ end.
+
+sd_dns(Domain) -> dnsname(<<"_dreki">>, Domain).
+node_dns(Domain) -> dnsname(<<"_node._dreki">>, Domain).
+name_dns(Domain) -> dnsname(<<"_name._dreki">>, Domain).
+node_name_dns(Domain) -> dnsname(<<"_name._node._dreki">>, Domain).
+
+dnsname(Prefix, Domain) when is_list(Prefix) ->
+ dnsname(list_to_binary(Prefix), Domain);
+dnsname(Prefix, Domain) when is_list(Domain) ->
+ dnsname(Prefix, list_to_binary(Domain));
+dnsname(Prefix, Domain) ->
+ Full = <<Prefix/binary, ".", Domain/binary>>,
+ binary:bin_to_list(Full).
+
+name(Host) when is_list(Host) ->
+ name(list_to_binary(Host));
+name(Host) ->
+ hd(binary:split(Host, <<".">>)).
diff --git a/apps/dreki/src/dreki_world_plum_events.erl b/apps/dreki/src/dreki_world_plum_events.erl
new file mode 100644
index 0000000..b32ae09
--- /dev/null
+++ b/apps/dreki/src/dreki_world_plum_events.erl
@@ -0,0 +1,17 @@
+-module(dreki_world_plum_events).
+-behaviour(gen_event).
+-export([init/1, handle_call/2, handle_event/2, terminate/2]).
+
+init([Server]) ->
+ {ok, Server}.
+
+handle_call(_, Server) ->
+ {ok, error, Server}.
+
+handle_event(Event, Server) ->
+ logger:info("plum_event: ~p", [Event]),
+ dreki_world_server:send_event(Server, {plum_events, Event}),
+ {ok, Server}.
+
+terminate(_, Server) ->
+ ok.
diff --git a/apps/dreki/src/dreki_world_server.erl b/apps/dreki/src/dreki_world_server.erl
new file mode 100644
index 0000000..2bc41ed
--- /dev/null
+++ b/apps/dreki/src/dreki_world_server.erl
@@ -0,0 +1,63 @@
+-module(dreki_world_server).
+-behaviour(partisan_gen_fsm).
+-include_lib("kernel/include/logger.hrl").
+
+-export([start_link/1, send_event/2]).
+-export([init/1]).
+-export([setup/2]).
+-export([wait/2]).
+
+-record(data, {
+ path=undefined
+}).
+
+start_link(Args) ->
+ partisan_gen_fsm:start_link({local, ?MODULE}, ?MODULE, Args, []).
+
+send_event(Name, Event) ->
+ partisan_gen_fsm:send_event(Name, Event).
+
+init(Args) ->
+ ok = plum_db_events:add_handler(dreki_world_plum_events, [?MODULE]),
+ Data = #data{path = dreki_world:path()},
+ ok = setup_autojoin(),
+ todo = setup_connect_nearest(),
+ ok = dreki_node:ensure_local_node(),
+ {ok, setup, Data, 0}.
+
+setup(timeout, Data) ->
+ ?LOG_INFO("world_server: setup done"),
+ {next_state, wait, Data}.
+
+setup_autojoin() ->
+ case application:get_env(dreki, autojoin, undefined) of
+ undefined -> ok;
+ List -> [dreki_peer_service:join(N) || N <- List]
+ end,
+ ok.
+
+setup_connect_nearest() ->
+ case dreki_peer_service:connect_nearest_dns() of
+ ok -> ok;
+ todo -> todo;
+ error ->
+ logger:error("Node did not join anyone from the world!!"),
+ error
+ end.
+
+wait({plum_events, {object_update, {Prefix, Key}, Object}}, Data) ->
+ process_object_update(Prefix, Key, Object, Data),
+ {next_state, wait, Data};
+wait(Event, Data) ->
+ ?LOG_INFO("world_server wait event: ~p", [Event]),
+ {next_state, wait, Data}.
+
+process_object_update({nodes, Node}, Key, _Obj, #data{path = Node}) ->
+ ?LOG_INFO("My node has been updated !"),
+ ok;
+process_object_update({nodes, Node}, Key, _Obj, _Data) ->
+ ?LOG_INFO("Node update ~p ~p", [Node, Key]),
+ ok;
+process_object_update(Prefix, Key, _Obj, _Data) ->
+ ?LOG_INFO("Metadata update ~p ~p", [Prefix, Key]),
+ ok.
diff --git a/apps/dreki/src/dreki_world_store.erl b/apps/dreki/src/dreki_world_store.erl
new file mode 100644
index 0000000..71ef2ce
--- /dev/null
+++ b/apps/dreki/src/dreki_world_store.erl
@@ -0,0 +1,48 @@
+-module(dreki_world_store).
+-include("dreki.hrl").
+-behaviour(dradis_store_backend).
+
+%% Types
+
+-record(store, {}).
+-type db() :: #store{}.
+-type args() :: #{}.
+
+-export([start/0, start/4, checkout/1, checkin/1, stop/0, stop/1]).
+-export([valid_store/5]).
+-export([list/1, count/1, exists/2, get/2, create/2, update/2, delete/2]).
+
+valid_store(_Namespace, Location, _Name, _NSMod, _Args) ->
+ case Location =:= dreki_world:root_path() of
+ true -> ok;
+ false -> {error, {dreki_world_store_invalid_location, Location, dreki_world:root_path()}}
+ end.
+
+start() -> ok.
+start(_, _, _, _) -> {ok, dreki_world_store}.
+
+checkout(_) -> {ok, #store{}}.
+checkin(_) -> ok.
+stop() -> ok.
+stop(_) -> ok.
+
+list(_) ->
+ {error, not_implemented}.
+
+count(_) ->
+ {error, not_implemented}.
+
+exists(_, _) ->
+ {error, not_implemented}.
+
+get(_, _) ->
+ {error, not_implemented}.
+
+delete(_, _) ->
+ {error, not_implemented}.
+
+create(_, _) ->
+ {error, not_implemented}.
+
+update(_Tab, _) ->
+ {error, not_implemented}.
diff --git a/apps/dreki/src/dreki_world_tasks.erl b/apps/dreki/src/dreki_world_tasks.erl
new file mode 100644
index 0000000..c27a7dc
--- /dev/null
+++ b/apps/dreki/src/dreki_world_tasks.erl
@@ -0,0 +1,66 @@
+-module(dreki_world_tasks).
+-include("dreki.hrl").
+-define(PREFIX, {objects, 'dreki_world_tasks'}).
+
+%% Types
+-type db() :: term().
+-type args() :: #{db_name => binary()}.
+
+%% storage-specific
+-export([start/1, open/1, sync/1, close/1, stop/1]).
+-export([all/1, count/1, exists/2, get/2, create/2, update/2, delete/2]).
+
+start(_) -> {ok, dreki_world_tasks}.
+open(_) -> {ok, dreki_world_tasks}.
+close(_) -> ok.
+stop(_) -> ok.
+sync(_) -> noop.
+
+all(_) ->
+ Results = plum_db:fold(fun ({_, Value}, Acc) ->
+ [Value | Acc]
+ end, [], ?PREFIX),
+ {ok, Results}.
+
+count(_) ->
+ Results = plum_db:fold(fun (_, Acc) ->
+ Acc + 1
+ end, 0, {objects, 'dreki_world_tasks'}),
+ {ok, Results}.
+
+exists(T, Id) ->
+ case get(T, Id) of
+ {ok, _} -> true;
+ _ -> false
+ end.
+
+get(_, Id) ->
+ case dreki_world:get(?PREFIX, Id) of
+ {ok, Val} -> {ok, Val};
+ not_found -> {error, {task_not_found, Id}}
+ end.
+
+create(T, Task = #dreki_task{id = Id}) ->
+ case get(T, Id) of
+ {ok, _} -> {error, {exists, Id}};
+ {error, {task_not_found, _}} ->
+ ok = plum_db:put(?PREFIX, Id, T),
+ ok = dreki_world:refresh_index(Task),
+ get(T, Id)
+ end.
+
+update(T, Task = #dreki_task{id = Id}) ->
+ case get(T, Id) of
+ Error = {error, {task_not_found, _}} -> Error;
+ {ok, _OldTask} ->
+ ok = plum_db:put(?PREFIX, Id, T),
+ ok = dreki_world:refresh_index(Task),
+ get(T, Id)
+ end.
+
+delete(T, #dreki_task{id = Id}) ->
+ {error, {dreki_world_tasks, delete_not_implemented}}.
+
+
+refresh_index(#dreki_task{uri = URI, tags = Tags, roles = Roles}) ->
+ dreki_world:refresh_index(tasks, #{uri => URI, tags => Tags, roles => Roles}).
diff --git a/apps/dreki_web/.gitignore b/apps/dreki_web/.gitignore
new file mode 100644
index 0000000..8dec94f
--- /dev/null
+++ b/apps/dreki_web/.gitignore
@@ -0,0 +1,20 @@
+.rebar3
+_*
+.eunit
+*.o
+*.beam
+*.plt
+*.swp
+*.swo
+.erlang.cookie
+ebin
+log
+erl_crash.dump
+.rebar
+logs
+_build
+.idea
+*.iml
+rebar3.crashdump
+*~
+/priv/static/
diff --git a/apps/dreki_web/Makefile b/apps/dreki_web/Makefile
new file mode 100644
index 0000000..f6bd7fe
--- /dev/null
+++ b/apps/dreki_web/Makefile
@@ -0,0 +1,93 @@
+DEST=priv/static
+
+NODE_ENV=production
+
+JS_TARGETS=\
+ app.js
+
+EDBUILD_FLAGS=--bundle --target=es2016 --minify
+ESBUILD_WATCH_FLAGS=--bundle --target=es2016 --sourcemap=inline
+
+CSS_TARGETS=\
+ app.css
+
+TAILWINDCSS_FLAGS=--postcss --minify
+TAILWINDCSS_WATCH_FLAGS=--postcss
+
+DIR_TARGETS=\
+ assets/images:images
+
+STATIC_TARGETS=\
+ assets/node_modules/@hpcc-js/wasm/dist/graphvizlib.wasm:graphvizlib.wasm\
+ assets/node_modules/@hpcc-js/wasm/dist/index.min.js:hpcc.wasm.min.js\
+ assets/images/favicon.ico:favicon.ico
+
+.export NODE_ENV
+
+TARGETS=
+COMPRESS=
+PHONIES=
+
+.for _t in ${JS_TARGETS}
+#.info "[JS] ${_t:S/^/assets\//} --> ${_t:S/^/${DEST}\//}"
+${_t:S/^/${DEST}\//}: ${_t:S/^/assets\//} assets/lib
+ @mkdir -p ${_t:S/^/${DEST}\//:H}
+ cd assets; npx esbuild ${_t} ${ESBUILD_FLAGS} --outfile=../${_t:S/^/${DEST}\//}
+COMPRESS+=${_t:S/^/${DEST}\//}
+
+${_t:S/^/watch@/}:
+ @mkdir -p ${_t:S/^/${DEST}\//:H}
+ cd assets; npx esbuild ${_t:S/assets\//} --watch ${ESBUILD_WATCH_FLAGS} --outfile=../${_t:S/^/${DEST}\//}
+PHONIES+=${_t:S/^/watch@/}
+.endfor
+
+.for _t in ${CSS_TARGETS}
+#.info "[CSS] ${_t:S/^/assets\//} --> ${_t:S/^/${DEST}\//}"
+${_t:S/^/${DEST}\//}: ${_t:S/^/assets\//} assets/css
+ @mkdir -p ${_t:S/^/${DEST}\//:H}
+ cd assets; npx tailwindcss ${TAILWINDCSS_FLAGS} -i ${_t} -o ${_t:S/^/${DEST}\//}
+COMPRESS+=${_t:S/^/${DEST}\//}
+
+${_t:S/^/watch@/}: ${_t:S/^/assets\//}
+ @mkdir -p ${_t:S/^/${DEST}\//:H}
+ cd assets; npx tailwindcss --watch ${TAILWINDCSS_WATCH_FLAGS} -i ${_t} -o ${_t:S/^/${DEST}\//}
+PHONIES+=${_t:S/^/watch@/}
+.endfor
+
+.for _t in ${DIR_TARGETS}
+#.info "[DIR] ${_t:C/\:.*$/\//} --> ${_t:C/^.*\:/${DEST}\//:S/$/\//}"
+${_t:C/^.*\://:S/\//_/:S/^/_build\/.make-assets-dir_/}: ${_t:C/\:.*$/\//}
+ @mkdir -p ${_t:C/^.*\:/${DEST}\//:S/$/\//:H}
+ rsync --archive --delete ${_t:C/\:.*$/\//} ${_t:C/^.*\:/${DEST}\//:S/$/\//}
+ @touch ${_t:C/^.*\://:S/\//_/:S/^/_build\/.make-assets-dir_/}
+TARGETS+=${_t:C/^.*\://:S/\//_/:S/^/_build\/.make-assets-dir_/}
+.endfor
+
+.for _t in ${STATIC_TARGETS}
+#.info "[STATIC] ${_t:C/\:.*$//} -> ${_t:C/^.*\:/${DEST}\//}"
+${_t:C/^.*\:/${DEST}\//}: ${_t:C/\:.*$//}
+ @mkdir -p ${_t:C/^.*\:/${DEST}\//:H}
+ cp ${_t:C/\:.*$//} ${_t:C/^.*\:/${DEST}\//}
+TARGETS+=${_t:C/^.*\:/${DEST}\//}
+.endfor
+
+.for _c in ${COMPRESS}
+#.info "[COMPRESS] ${_c}"
+${_c:S/$/.gz/}: ${_c}
+ gzip --force --keep ${.ALLSRC}
+TARGETS+=${_c:S/$/.gz/}
+${_c:S/$/.br/}: ${_c}
+ brotli --force --keep ${.ALLSRC}
+TARGETS+=${_c:S/$/.br/}
+.endfor
+
+#.info "Compressable: ${COMPRESS}"
+#.info "Targets: ${TARGETS}"
+
+all: ${TARGETS}
+
+clean:
+ rm -r ${DEST}
+
+.MAIN: all
+.PHONY: all clean ${PHONIES}
diff --git a/apps/dreki_web/README.md b/apps/dreki_web/README.md
new file mode 100644
index 0000000..4de4e7f
--- /dev/null
+++ b/apps/dreki_web/README.md
@@ -0,0 +1,9 @@
+dreki_web
+=====
+
+An OTP application
+
+Build
+-----
+
+ $ rebar3 compile
diff --git a/apps/dreki_web/assets/app.css b/apps/dreki_web/assets/app.css
new file mode 100644
index 0000000..44e75dc
--- /dev/null
+++ b/apps/dreki_web/assets/app.css
@@ -0,0 +1,30 @@
+@import "tailwindcss/base";
+@import "tailwindcss/components";
+@import "tailwindcss/utilities";
+
+
+.btn {
+ @apply px-4 py-2 inline-flex items-center border border-transparent focus:outline-none focus:ring-2 focus:ring-offset-2 text-base font-medium;
+}
+.btn-primary {
+ @apply bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500;
+}
+.btn-secondary {
+ @apply bg-primary-100 text-primary-700 hover:bg-secondary-200 focus:ring-primary-500;
+}
+.btn-danger {
+ @apply bg-red-600 text-white hover:bg-red-700 focus:ring-red-500;
+}
+.btn-white {
+ @apply border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:ring-primary-500;
+}
+.btn-xs {
+ @apply px-2.5 py-1.5 text-xs font-medium rounded shadow-sm;
+}
+.btn-sm {
+ @apply px-3 py-2 text-sm leading-4 font-medium shadow-sm;
+}
+
+ul.nav-tree li.node {
+ @apply font-bold;
+}
diff --git a/apps/dreki_web/assets/app.js b/apps/dreki_web/assets/app.js
new file mode 100644
index 0000000..341b926
--- /dev/null
+++ b/apps/dreki_web/assets/app.js
@@ -0,0 +1,9 @@
+import * as Turbo from "@hotwired/turbo";
+import { Application } from "@hotwired/stimulus"
+
+import GraphvizController from "./lib/controllers/graphviz_controller";
+
+window.Stimulus = Application.start();
+Stimulus.register("graphviz", GraphvizController);
+
+console.log("pouet");
diff --git a/apps/dreki_web/assets/images/android-chrome-192x192.png b/apps/dreki_web/assets/images/android-chrome-192x192.png
new file mode 100644
index 0000000..0372bed
--- /dev/null
+++ b/apps/dreki_web/assets/images/android-chrome-192x192.png
Binary files differ
diff --git a/apps/dreki_web/assets/images/android-chrome-512x512.png b/apps/dreki_web/assets/images/android-chrome-512x512.png
new file mode 100644
index 0000000..1ee52c0
--- /dev/null
+++ b/apps/dreki_web/assets/images/android-chrome-512x512.png
Binary files differ
diff --git a/apps/dreki_web/assets/images/apple-touch-icon.png b/apps/dreki_web/assets/images/apple-touch-icon.png
new file mode 100644
index 0000000..a6523ab
--- /dev/null
+++ b/apps/dreki_web/assets/images/apple-touch-icon.png
Binary files differ
diff --git a/apps/dreki_web/assets/images/favicon-16x16.png b/apps/dreki_web/assets/images/favicon-16x16.png
new file mode 100644
index 0000000..35a83ac
--- /dev/null
+++ b/apps/dreki_web/assets/images/favicon-16x16.png
Binary files differ
diff --git a/apps/dreki_web/assets/images/favicon-32x32.png b/apps/dreki_web/assets/images/favicon-32x32.png
new file mode 100644
index 0000000..cf111b2
--- /dev/null
+++ b/apps/dreki_web/assets/images/favicon-32x32.png
Binary files differ
diff --git a/apps/dreki_web/assets/images/favicon.ico b/apps/dreki_web/assets/images/favicon.ico
new file mode 100644
index 0000000..1a1e452
--- /dev/null
+++ b/apps/dreki_web/assets/images/favicon.ico
Binary files differ
diff --git a/apps/dreki_web/assets/lib/controllers/graphviz_controller.js b/apps/dreki_web/assets/lib/controllers/graphviz_controller.js
new file mode 100644
index 0000000..7023b35
--- /dev/null
+++ b/apps/dreki_web/assets/lib/controllers/graphviz_controller.js
@@ -0,0 +1,57 @@
+import { Controller } from "@hotwired/stimulus"
+import * as d3 from 'd3'
+import graphviz from 'd3-graphviz'
+
+window.d3 = d3;
+window.graphviz = graphviz;
+
+export default class extends Controller {
+ static targets = ["loading", "error", "graph"]
+ static values = {
+ url: String
+ }
+
+ static graphvizOptions = {
+ useWorker: false,
+ useSharedWorker: false,
+ engine: 'dot',
+ keyMode: 'title',
+ fade: true,
+ tweenPaths: true,
+ tweenShapes: true,
+ convertEqualSidedPolygons: true,
+ tweenPrecision: 1,
+ growEnteringEdges: true,
+ zoom: false,
+ zoomScaleExtent: [0.1, 10],
+ zoomTranslateExtent: [[-Infinity, -Infinity], [+Infinity, +Infinity]],
+ width: null,
+ height: null,
+ scale: 1,
+ fit: true
+ }
+
+ connect() {
+ console.log("loading graph", this.element.id, this.urlValue)
+ fetch(this.urlValue).then((response) => {
+ if (!response.ok) {
+ throw new Error(`Failed to load graph: ${response.status}`)
+ }
+ return response.text()
+ }).then((text) => {
+ return this.load(text)
+ }).catch((error) => {
+ this.loadingTarget.classList.add("hidden");
+ this.errorTarget.classList.remove("hidden");
+ console.log("graph_controller", this.urlValue, error)
+ });
+ }
+
+ load(data) {
+ console.log("Data loaded...", data, this.graphTarget.id);
+ this.loadingTarget.classList.add("hidden");
+ var target = d3.select("#" + this.graphTarget.id);
+ graphviz.graphviz("#" + this.graphTarget.id, this.graphvizOptions).dot(data).render();
+ }
+
+}
diff --git a/apps/dreki_web/assets/package-lock.json b/apps/dreki_web/assets/package-lock.json
new file mode 100644
index 0000000..c99c8b3
--- /dev/null
+++ b/apps/dreki_web/assets/package-lock.json
@@ -0,0 +1,3603 @@
+{
+ "name": "dreki-assets",
+ "version": "0.0.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "dreki-assets",
+ "version": "0.0.0",
+ "dependencies": {
+ "@tailwindcss/aspect-ratio": "^0.4.0",
+ "@tailwindcss/forms": "^0.5.0",
+ "@tailwindcss/line-clamp": "^0.3.1",
+ "@tailwindcss/typography": "^0.5.2",
+ "crossfilter": "^1.3.12"
+ },
+ "devDependencies": {
+ "@hotwired/stimulus": "^3.0.1",
+ "@hotwired/turbo": "^7.1.0",
+ "autoprefixer": "^10.4.4",
+ "concurrently": "^6.2.0",
+ "d3": "^7.4.0",
+ "d3-graphviz": "^4.1.0",
+ "esbuild": "^0.12.17",
+ "postcss": "^8.4.12",
+ "postcss-import": "^14.0.2",
+ "tailwindcss": "^3.0.23"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.16.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz",
+ "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==",
+ "dependencies": {
+ "@babel/highlight": "^7.16.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.16.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
+ "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.16.10",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz",
+ "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.16.7",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ },
+ "node_modules/@babel/highlight/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@hotwired/stimulus": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.0.1.tgz",
+ "integrity": "sha512-oHsJhgY2cip+K2ED7vKUNd2P+BEswVhrCYcJ802DSsblJFv7mPFVk3cQKvm2vHgHeDVdnj7oOKrBbzp1u8D+KA==",
+ "dev": true
+ },
+ "node_modules/@hotwired/turbo": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@hotwired/turbo/-/turbo-7.1.0.tgz",
+ "integrity": "sha512-Q8kGjqwPqER+CtpQudbH+3Zgs2X4zb6pBAlr6NsKTXadg45pAOvxI9i4QpuHbwSzR2+x87HUm+rot9F/Pe8rxA==",
+ "dev": true
+ },
+ "node_modules/@hpcc-js/wasm": {
+ "version": "1.12.8",
+ "resolved": "https://registry.npmjs.org/@hpcc-js/wasm/-/wasm-1.12.8.tgz",
+ "integrity": "sha512-n4q9ARKco2hpCLsuVaW6Az3cDVaua7B3DSONHkc49WtEzgY/btvcDG5Zr1P6PZDv0sQ7oPnAi9Y+W2DI++MgcQ==",
+ "dev": true,
+ "dependencies": {
+ "yargs": "^17.3.1"
+ },
+ "bin": {
+ "dot-wasm": "bin/cli.js"
+ }
+ },
+ "node_modules/@hpcc-js/wasm/node_modules/yargs": {
+ "version": "17.4.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.0.tgz",
+ "integrity": "sha512-WJudfrk81yWFSOkZYpAZx4Nt7V4xp7S/uJkX0CnxovMCt1wCE8LNftPpNuF9X/u9gN5nsD7ycYtRcDf2pL3UiA==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@hpcc-js/wasm/node_modules/yargs-parser": {
+ "version": "21.0.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz",
+ "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@tailwindcss/aspect-ratio": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.0.tgz",
+ "integrity": "sha512-WJu0I4PpqNPuutpaA9zDUq2JXR+lorZ7PbLcKNLmb6GL9/HLfC7w3CRsMhJF4BbYd/lkY6CfXOvkYpuGnZfkpQ==",
+ "peerDependencies": {
+ "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
+ }
+ },
+ "node_modules/@tailwindcss/forms": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.0.tgz",
+ "integrity": "sha512-KzWugryEBFkmoaYcBE18rs6gthWCFHHO7cAZm2/hv3hwD67AzwP7udSCa22E7R1+CEJL/FfhYsJWrc0b1aeSzw==",
+ "dependencies": {
+ "mini-svg-data-uri": "^1.2.3"
+ },
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
+ }
+ },
+ "node_modules/@tailwindcss/line-clamp": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.3.1.tgz",
+ "integrity": "sha512-pNr0T8LAc3TUx/gxCfQZRe9NB2dPEo/cedPHzUGIPxqDMhgjwNm6jYxww4W5l0zAsAddxr+XfZcqttGiFDgrGg==",
+ "peerDependencies": {
+ "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
+ }
+ },
+ "node_modules/@tailwindcss/typography": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.2.tgz",
+ "integrity": "sha512-coq8DBABRPFcVhVIk6IbKyyHUt7YTEC/C992tatFB+yEx5WGBQrCgsSFjxHUr8AWXphWckadVJbominEduYBqw==",
+ "dependencies": {
+ "lodash.castarray": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.merge": "^4.6.2"
+ },
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || insiders"
+ }
+ },
+ "node_modules/@types/parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
+ },
+ "node_modules/acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-node": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz",
+ "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==",
+ "dependencies": {
+ "acorn": "^7.0.0",
+ "acorn-walk": "^7.0.0",
+ "xtend": "^4.0.2"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
+ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+ "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz",
+ "integrity": "sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA=="
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.4",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.4.tgz",
+ "integrity": "sha512-Tm8JxsB286VweiZ5F0anmbyGiNI3v3wGv3mz9W+cxEDYB/6jbnj6GM9H9mK3wIL8ftgl+C07Lcwb8PG5PCCPzA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ }
+ ],
+ "dependencies": {
+ "browserslist": "^4.20.2",
+ "caniuse-lite": "^1.0.30001317",
+ "fraction.js": "^4.2.0",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.0.0",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.20.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz",
+ "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ }
+ ],
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001317",
+ "electron-to-chromium": "^1.4.84",
+ "escalade": "^3.1.1",
+ "node-releases": "^2.0.2",
+ "picocolors": "^1.0.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001323",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001323.tgz",
+ "integrity": "sha512-e4BF2RlCVELKx8+RmklSEIVub1TWrmdhvA5kEUueummz1XyySW0DVk+3x9HyhU9MuWTa2BhqLgEuEmUwASAdCA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ }
+ ]
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/concurrently": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.5.1.tgz",
+ "integrity": "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "date-fns": "^2.16.1",
+ "lodash": "^4.17.21",
+ "rxjs": "^6.6.3",
+ "spawn-command": "^0.0.2-1",
+ "supports-color": "^8.1.0",
+ "tree-kill": "^1.2.2",
+ "yargs": "^16.2.0"
+ },
+ "bin": {
+ "concurrently": "bin/concurrently.js"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/cosmiconfig": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
+ "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==",
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/crossfilter": {
+ "version": "1.3.12",
+ "resolved": "https://registry.npmjs.org/crossfilter/-/crossfilter-1.3.12.tgz",
+ "integrity": "sha1-FH1yNqmMRcafeL3DqZ1vsA9wkww="
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/d3": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.4.0.tgz",
+ "integrity": "sha512-/xKyIYpKzd+I2DhiS2ANYJtEfHkE9lHKBFwqsplKsazPcXy2N1KIJSMTJsRk42jHbHCH0KPJGd0RnBt6NBJ1MA==",
+ "dev": true,
+ "dependencies": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "3",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.1.3.tgz",
+ "integrity": "sha512-qwW+3phQqmogYyRDI9hC8AWNeTdyYexazuq/dDM7QgujVDVexwNOxVMUJt+0BRpCw/4S4+byuYTV51kttK/Bzw==",
+ "dev": true,
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "dev": true,
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush/node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "dev": true,
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush/node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush/node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "dev": true,
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "dev": true,
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz",
+ "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==",
+ "dev": true
+ },
+ "node_modules/d3-contour": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-3.0.1.tgz",
+ "integrity": "sha512-0Oc4D0KyhwhM7ZL0RMnfGycLN7hxHB8CMmwZ3+H26PWAG0ozNuYG5hXSDNgmP1SgJkQMrlG6cP20HoaSbvcJTQ==",
+ "dev": true,
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz",
+ "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==",
+ "dev": true,
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz",
+ "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==",
+ "dev": true
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "dev": true,
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-2.0.0.tgz",
+ "integrity": "sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ==",
+ "dev": true
+ },
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "dev": true,
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "dev": true,
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz",
+ "integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==",
+ "dev": true
+ },
+ "node_modules/d3-geo": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.0.1.tgz",
+ "integrity": "sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==",
+ "dev": true,
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-graphviz": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-graphviz/-/d3-graphviz-4.1.0.tgz",
+ "integrity": "sha512-RtCGnEROcte5npTfjhiNR3kSbwhBl8LA6NNq0oFRXEIFB9N4xHQgGjZWfAHVsN9NQVctEEkRQ4H0GGdqfStlZQ==",
+ "dev": true,
+ "dependencies": {
+ "@hpcc-js/wasm": "1.12.8",
+ "d3-dispatch": "^2.0.0",
+ "d3-format": "^2.0.0",
+ "d3-interpolate": "^2.0.1",
+ "d3-path": "^2.0.0",
+ "d3-timer": "^2.0.0",
+ "d3-transition": "^2.0.0",
+ "d3-zoom": "^2.0.0"
+ },
+ "peerDependencies": {
+ "d3-selection": "^2.0.0"
+ }
+ },
+ "node_modules/d3-graphviz/node_modules/d3-dispatch": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz",
+ "integrity": "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==",
+ "dev": true
+ },
+ "node_modules/d3-graphviz/node_modules/d3-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz",
+ "integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==",
+ "dev": true
+ },
+ "node_modules/d3-graphviz/node_modules/d3-timer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz",
+ "integrity": "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==",
+ "dev": true
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.1.tgz",
+ "integrity": "sha512-LtAIu54UctRmhGKllleflmHalttH3zkfSi4NlKrTAoFKjC+AFBJohsCAdgCBYQwH0F8hIOGY89X1pPqAchlMkA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz",
+ "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==",
+ "dev": true,
+ "dependencies": {
+ "d3-color": "1 - 2"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
+ "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==",
+ "dev": true
+ },
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "dev": true,
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz",
+ "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==",
+ "dev": true,
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-2.0.0.tgz",
+ "integrity": "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA==",
+ "dev": true
+ },
+ "node_modules/d3-time": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz",
+ "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==",
+ "dev": true,
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "dev": true,
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz",
+ "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==",
+ "dev": true
+ },
+ "node_modules/d3-transition": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-2.0.0.tgz",
+ "integrity": "sha512-42ltAGgJesfQE3u9LuuBHNbGrI/AJjNL2OAUdclE70UE6Vy239GCBEYD38uBPoLeNsOhFStGpPI0BAOV+HMxog==",
+ "dev": true,
+ "dependencies": {
+ "d3-color": "1 - 2",
+ "d3-dispatch": "1 - 2",
+ "d3-ease": "1 - 2",
+ "d3-interpolate": "1 - 2",
+ "d3-timer": "1 - 2"
+ },
+ "peerDependencies": {
+ "d3-selection": "2"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-2.0.0.tgz",
+ "integrity": "sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw==",
+ "dev": true,
+ "dependencies": {
+ "d3-dispatch": "1 - 2",
+ "d3-drag": "2",
+ "d3-interpolate": "1 - 2",
+ "d3-selection": "2",
+ "d3-transition": "2"
+ }
+ },
+ "node_modules/d3-zoom/node_modules/d3-drag": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-2.0.0.tgz",
+ "integrity": "sha512-g9y9WbMnF5uqB9qKqwIIa/921RYWzlUDv9Jl1/yONQwxbOfszAWTCm8u7HOTgJgRDXiRZN56cHT9pd24dmXs8w==",
+ "dev": true,
+ "dependencies": {
+ "d3-dispatch": "1 - 2",
+ "d3-selection": "2"
+ }
+ },
+ "node_modules/d3/node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "dev": true,
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "dev": true,
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-path": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz",
+ "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-shape": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz",
+ "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==",
+ "dev": true,
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3/node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "dev": true,
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3/node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "dev": true,
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/date-fns": {
+ "version": "2.28.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
+ "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.11"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/date-fns"
+ }
+ },
+ "node_modules/defined": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
+ "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM="
+ },
+ "node_modules/delaunator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz",
+ "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==",
+ "dev": true,
+ "dependencies": {
+ "robust-predicates": "^3.0.0"
+ }
+ },
+ "node_modules/detective": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz",
+ "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==",
+ "dependencies": {
+ "acorn-node": "^1.6.1",
+ "defined": "^1.0.0",
+ "minimist": "^1.1.1"
+ },
+ "bin": {
+ "detective": "bin/detective.js"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.4.103",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.103.tgz",
+ "integrity": "sha512-c/uKWR1Z/W30Wy/sx3dkZoj4BijbXX85QKWu9jJfjho3LBAXNEGAEW3oWiGb+dotA6C6BzCTxL2/aLes7jlUeg=="
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.12.29",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz",
+ "integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/fast-glob": {
+ "version": "3.2.11",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
+ "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
+ "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
+ "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://www.patreon.com/infusion"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
+ "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
+ },
+ "node_modules/lilconfig": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz",
+ "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true
+ },
+ "node_modules/lodash.castarray": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
+ "integrity": "sha1-wCUTUV4wna3dTCTGDP3c9ZdtkRU="
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mini-svg-data-uri": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
+ "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
+ "bin": {
+ "mini-svg-data-uri": "cli.js"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.2.tgz",
+ "integrity": "sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz",
+ "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg=="
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
+ "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.12",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.12.tgz",
+ "integrity": "sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.1",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz",
+ "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==",
+ "dev": true,
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz",
+ "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.3.3"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
+ "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
+ "dependencies": {
+ "lilconfig": "^2.0.5",
+ "yaml": "^1.10.2"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": ">=8.0.9",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz",
+ "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.6"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.0.10",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
+ "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=",
+ "dev": true,
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
+ "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
+ "dependencies": {
+ "is-core-module": "^2.8.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/robust-predicates": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.1.tgz",
+ "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==",
+ "dev": true
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=",
+ "dev": true
+ },
+ "node_modules/rxjs": {
+ "version": "6.6.7",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
+ "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "node_modules/source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/spawn-command": {
+ "version": "0.0.2-1",
+ "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
+ "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=",
+ "dev": true
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.0.23",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.23.tgz",
+ "integrity": "sha512-+OZOV9ubyQ6oI2BXEhzw4HrqvgcARY38xv3zKcjnWtMIZstEsXdI9xftd1iB7+RbOnj2HOEzkA0OyB5BaSxPQA==",
+ "dependencies": {
+ "arg": "^5.0.1",
+ "chalk": "^4.1.2",
+ "chokidar": "^3.5.3",
+ "color-name": "^1.1.4",
+ "cosmiconfig": "^7.0.1",
+ "detective": "^5.2.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.2.11",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^2.2.0",
+ "postcss": "^8.4.6",
+ "postcss-js": "^4.0.0",
+ "postcss-load-config": "^3.1.0",
+ "postcss-nested": "5.0.6",
+ "postcss-selector-parser": "^6.0.9",
+ "postcss-value-parser": "^4.2.0",
+ "quick-lru": "^5.1.1",
+ "resolve": "^1.22.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=12.13.0"
+ },
+ "peerDependencies": {
+ "autoprefixer": "^10.0.2",
+ "postcss": "^8.0.9"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "20.2.9",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ }
+ },
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.16.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz",
+ "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==",
+ "requires": {
+ "@babel/highlight": "^7.16.7"
+ }
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.16.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
+ "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw=="
+ },
+ "@babel/highlight": {
+ "version": "7.16.10",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz",
+ "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==",
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.16.7",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "@hotwired/stimulus": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.0.1.tgz",
+ "integrity": "sha512-oHsJhgY2cip+K2ED7vKUNd2P+BEswVhrCYcJ802DSsblJFv7mPFVk3cQKvm2vHgHeDVdnj7oOKrBbzp1u8D+KA==",
+ "dev": true
+ },
+ "@hotwired/turbo": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@hotwired/turbo/-/turbo-7.1.0.tgz",
+ "integrity": "sha512-Q8kGjqwPqER+CtpQudbH+3Zgs2X4zb6pBAlr6NsKTXadg45pAOvxI9i4QpuHbwSzR2+x87HUm+rot9F/Pe8rxA==",
+ "dev": true
+ },
+ "@hpcc-js/wasm": {
+ "version": "1.12.8",
+ "resolved": "https://registry.npmjs.org/@hpcc-js/wasm/-/wasm-1.12.8.tgz",
+ "integrity": "sha512-n4q9ARKco2hpCLsuVaW6Az3cDVaua7B3DSONHkc49WtEzgY/btvcDG5Zr1P6PZDv0sQ7oPnAi9Y+W2DI++MgcQ==",
+ "dev": true,
+ "requires": {
+ "yargs": "^17.3.1"
+ },
+ "dependencies": {
+ "yargs": {
+ "version": "17.4.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.0.tgz",
+ "integrity": "sha512-WJudfrk81yWFSOkZYpAZx4Nt7V4xp7S/uJkX0CnxovMCt1wCE8LNftPpNuF9X/u9gN5nsD7ycYtRcDf2pL3UiA==",
+ "dev": true,
+ "requires": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.0.0"
+ }
+ },
+ "yargs-parser": {
+ "version": "21.0.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz",
+ "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==",
+ "dev": true
+ }
+ }
+ },
+ "@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "requires": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ }
+ },
+ "@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="
+ },
+ "@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "requires": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ }
+ },
+ "@tailwindcss/aspect-ratio": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.0.tgz",
+ "integrity": "sha512-WJu0I4PpqNPuutpaA9zDUq2JXR+lorZ7PbLcKNLmb6GL9/HLfC7w3CRsMhJF4BbYd/lkY6CfXOvkYpuGnZfkpQ==",
+ "requires": {}
+ },
+ "@tailwindcss/forms": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.0.tgz",
+ "integrity": "sha512-KzWugryEBFkmoaYcBE18rs6gthWCFHHO7cAZm2/hv3hwD67AzwP7udSCa22E7R1+CEJL/FfhYsJWrc0b1aeSzw==",
+ "requires": {
+ "mini-svg-data-uri": "^1.2.3"
+ }
+ },
+ "@tailwindcss/line-clamp": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.3.1.tgz",
+ "integrity": "sha512-pNr0T8LAc3TUx/gxCfQZRe9NB2dPEo/cedPHzUGIPxqDMhgjwNm6jYxww4W5l0zAsAddxr+XfZcqttGiFDgrGg==",
+ "requires": {}
+ },
+ "@tailwindcss/typography": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.2.tgz",
+ "integrity": "sha512-coq8DBABRPFcVhVIk6IbKyyHUt7YTEC/C992tatFB+yEx5WGBQrCgsSFjxHUr8AWXphWckadVJbominEduYBqw==",
+ "requires": {
+ "lodash.castarray": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.merge": "^4.6.2"
+ }
+ },
+ "@types/parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
+ },
+ "acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
+ },
+ "acorn-node": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz",
+ "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==",
+ "requires": {
+ "acorn": "^7.0.0",
+ "acorn-walk": "^7.0.0",
+ "xtend": "^4.0.2"
+ }
+ },
+ "acorn-walk": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
+ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA=="
+ },
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+ "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "arg": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz",
+ "integrity": "sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA=="
+ },
+ "autoprefixer": {
+ "version": "10.4.4",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.4.tgz",
+ "integrity": "sha512-Tm8JxsB286VweiZ5F0anmbyGiNI3v3wGv3mz9W+cxEDYB/6jbnj6GM9H9mK3wIL8ftgl+C07Lcwb8PG5PCCPzA==",
+ "requires": {
+ "browserslist": "^4.20.2",
+ "caniuse-lite": "^1.0.30001317",
+ "fraction.js": "^4.2.0",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.0.0",
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "browserslist": {
+ "version": "4.20.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz",
+ "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==",
+ "requires": {
+ "caniuse-lite": "^1.0.30001317",
+ "electron-to-chromium": "^1.4.84",
+ "escalade": "^3.1.1",
+ "node-releases": "^2.0.2",
+ "picocolors": "^1.0.0"
+ }
+ },
+ "callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
+ },
+ "camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="
+ },
+ "caniuse-lite": {
+ "version": "1.0.30001323",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001323.tgz",
+ "integrity": "sha512-e4BF2RlCVELKx8+RmklSEIVub1TWrmdhvA5kEUueummz1XyySW0DVk+3x9HyhU9MuWTa2BhqLgEuEmUwASAdCA=="
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "dependencies": {
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "requires": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "fsevents": "~2.3.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "dependencies": {
+ "glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ }
+ }
+ },
+ "cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "dev": true
+ },
+ "concurrently": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.5.1.tgz",
+ "integrity": "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag==",
+ "dev": true,
+ "requires": {
+ "chalk": "^4.1.0",
+ "date-fns": "^2.16.1",
+ "lodash": "^4.17.21",
+ "rxjs": "^6.6.3",
+ "spawn-command": "^0.0.2-1",
+ "supports-color": "^8.1.0",
+ "tree-kill": "^1.2.2",
+ "yargs": "^16.2.0"
+ }
+ },
+ "cosmiconfig": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
+ "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==",
+ "requires": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ }
+ },
+ "crossfilter": {
+ "version": "1.3.12",
+ "resolved": "https://registry.npmjs.org/crossfilter/-/crossfilter-1.3.12.tgz",
+ "integrity": "sha1-FH1yNqmMRcafeL3DqZ1vsA9wkww="
+ },
+ "cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="
+ },
+ "d3": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.4.0.tgz",
+ "integrity": "sha512-/xKyIYpKzd+I2DhiS2ANYJtEfHkE9lHKBFwqsplKsazPcXy2N1KIJSMTJsRk42jHbHCH0KPJGd0RnBt6NBJ1MA==",
+ "dev": true,
+ "requires": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "3",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "dependencies": {
+ "d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "dev": true
+ },
+ "d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "dev": true
+ },
+ "d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "dev": true,
+ "requires": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ }
+ },
+ "d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "dev": true
+ },
+ "d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "dev": true
+ },
+ "d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "dev": true,
+ "requires": {
+ "d3-color": "1 - 3"
+ }
+ },
+ "d3-path": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz",
+ "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==",
+ "dev": true
+ },
+ "d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "dev": true
+ },
+ "d3-shape": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz",
+ "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==",
+ "dev": true,
+ "requires": {
+ "d3-path": "1 - 3"
+ }
+ },
+ "d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "dev": true
+ },
+ "d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "dev": true,
+ "requires": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ }
+ },
+ "d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "dev": true,
+ "requires": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ }
+ }
+ }
+ },
+ "d3-array": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.1.3.tgz",
+ "integrity": "sha512-qwW+3phQqmogYyRDI9hC8AWNeTdyYexazuq/dDM7QgujVDVexwNOxVMUJt+0BRpCw/4S4+byuYTV51kttK/Bzw==",
+ "dev": true,
+ "requires": {
+ "internmap": "1 - 2"
+ }
+ },
+ "d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "dev": true
+ },
+ "d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "dev": true,
+ "requires": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "dependencies": {
+ "d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "dev": true,
+ "requires": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ }
+ },
+ "d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "dev": true
+ },
+ "d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "dev": true,
+ "requires": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ }
+ }
+ }
+ },
+ "d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "dev": true,
+ "requires": {
+ "d3-path": "1 - 3"
+ }
+ },
+ "d3-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz",
+ "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==",
+ "dev": true
+ },
+ "d3-contour": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-3.0.1.tgz",
+ "integrity": "sha512-0Oc4D0KyhwhM7ZL0RMnfGycLN7hxHB8CMmwZ3+H26PWAG0ozNuYG5hXSDNgmP1SgJkQMrlG6cP20HoaSbvcJTQ==",
+ "dev": true,
+ "requires": {
+ "d3-array": "2 - 3"
+ }
+ },
+ "d3-delaunay": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz",
+ "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==",
+ "dev": true,
+ "requires": {
+ "delaunator": "5"
+ }
+ },
+ "d3-dispatch": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz",
+ "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==",
+ "dev": true
+ },
+ "d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "dev": true,
+ "requires": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ }
+ },
+ "d3-ease": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-2.0.0.tgz",
+ "integrity": "sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ==",
+ "dev": true
+ },
+ "d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "dev": true,
+ "requires": {
+ "d3-dsv": "1 - 3"
+ }
+ },
+ "d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "dev": true,
+ "requires": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ }
+ },
+ "d3-format": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz",
+ "integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==",
+ "dev": true
+ },
+ "d3-geo": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.0.1.tgz",
+ "integrity": "sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==",
+ "dev": true,
+ "requires": {
+ "d3-array": "2.5.0 - 3"
+ }
+ },
+ "d3-graphviz": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-graphviz/-/d3-graphviz-4.1.0.tgz",
+ "integrity": "sha512-RtCGnEROcte5npTfjhiNR3kSbwhBl8LA6NNq0oFRXEIFB9N4xHQgGjZWfAHVsN9NQVctEEkRQ4H0GGdqfStlZQ==",
+ "dev": true,
+ "requires": {
+ "@hpcc-js/wasm": "1.12.8",
+ "d3-dispatch": "^2.0.0",
+ "d3-format": "^2.0.0",
+ "d3-interpolate": "^2.0.1",
+ "d3-path": "^2.0.0",
+ "d3-timer": "^2.0.0",
+ "d3-transition": "^2.0.0",
+ "d3-zoom": "^2.0.0"
+ },
+ "dependencies": {
+ "d3-dispatch": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz",
+ "integrity": "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==",
+ "dev": true
+ },
+ "d3-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz",
+ "integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==",
+ "dev": true
+ },
+ "d3-timer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz",
+ "integrity": "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==",
+ "dev": true
+ }
+ }
+ },
+ "d3-hierarchy": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.1.tgz",
+ "integrity": "sha512-LtAIu54UctRmhGKllleflmHalttH3zkfSi4NlKrTAoFKjC+AFBJohsCAdgCBYQwH0F8hIOGY89X1pPqAchlMkA==",
+ "dev": true
+ },
+ "d3-interpolate": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz",
+ "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==",
+ "dev": true,
+ "requires": {
+ "d3-color": "1 - 2"
+ }
+ },
+ "d3-path": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
+ "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==",
+ "dev": true
+ },
+ "d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "dev": true
+ },
+ "d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "dev": true
+ },
+ "d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "dev": true
+ },
+ "d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "dev": true,
+ "requires": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ }
+ },
+ "d3-scale-chromatic": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz",
+ "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==",
+ "dev": true,
+ "requires": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ }
+ },
+ "d3-selection": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-2.0.0.tgz",
+ "integrity": "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA==",
+ "dev": true
+ },
+ "d3-time": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz",
+ "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==",
+ "dev": true,
+ "requires": {
+ "d3-array": "2 - 3"
+ }
+ },
+ "d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "dev": true,
+ "requires": {
+ "d3-time": "1 - 3"
+ }
+ },
+ "d3-timer": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz",
+ "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==",
+ "dev": true
+ },
+ "d3-transition": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-2.0.0.tgz",
+ "integrity": "sha512-42ltAGgJesfQE3u9LuuBHNbGrI/AJjNL2OAUdclE70UE6Vy239GCBEYD38uBPoLeNsOhFStGpPI0BAOV+HMxog==",
+ "dev": true,
+ "requires": {
+ "d3-color": "1 - 2",
+ "d3-dispatch": "1 - 2",
+ "d3-ease": "1 - 2",
+ "d3-interpolate": "1 - 2",
+ "d3-timer": "1 - 2"
+ }
+ },
+ "d3-zoom": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-2.0.0.tgz",
+ "integrity": "sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw==",
+ "dev": true,
+ "requires": {
+ "d3-dispatch": "1 - 2",
+ "d3-drag": "2",
+ "d3-interpolate": "1 - 2",
+ "d3-selection": "2",
+ "d3-transition": "2"
+ },
+ "dependencies": {
+ "d3-drag": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-2.0.0.tgz",
+ "integrity": "sha512-g9y9WbMnF5uqB9qKqwIIa/921RYWzlUDv9Jl1/yONQwxbOfszAWTCm8u7HOTgJgRDXiRZN56cHT9pd24dmXs8w==",
+ "dev": true,
+ "requires": {
+ "d3-dispatch": "1 - 2",
+ "d3-selection": "2"
+ }
+ }
+ }
+ },
+ "date-fns": {
+ "version": "2.28.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
+ "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
+ "dev": true
+ },
+ "defined": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
+ "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM="
+ },
+ "delaunator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz",
+ "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==",
+ "dev": true,
+ "requires": {
+ "robust-predicates": "^3.0.0"
+ }
+ },
+ "detective": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz",
+ "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==",
+ "requires": {
+ "acorn-node": "^1.6.1",
+ "defined": "^1.0.0",
+ "minimist": "^1.1.1"
+ }
+ },
+ "didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
+ },
+ "dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
+ },
+ "electron-to-chromium": {
+ "version": "1.4.103",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.103.tgz",
+ "integrity": "sha512-c/uKWR1Z/W30Wy/sx3dkZoj4BijbXX85QKWu9jJfjho3LBAXNEGAEW3oWiGb+dotA6C6BzCTxL2/aLes7jlUeg=="
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "requires": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "esbuild": {
+ "version": "0.12.29",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz",
+ "integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==",
+ "dev": true
+ },
+ "escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+ },
+ "fast-glob": {
+ "version": "3.2.11",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
+ "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
+ "requires": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "dependencies": {
+ "glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ }
+ }
+ },
+ "fastq": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
+ "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
+ "requires": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "fraction.js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
+ "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA=="
+ },
+ "fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "optional": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
+ "get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true
+ },
+ "glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "requires": {
+ "is-glob": "^4.0.3"
+ }
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+ },
+ "iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ }
+ },
+ "import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "requires": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ }
+ },
+ "internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "dev": true
+ },
+ "is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
+ },
+ "is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-core-module": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
+ "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
+ },
+ "lilconfig": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz",
+ "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg=="
+ },
+ "lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
+ },
+ "lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true
+ },
+ "lodash.castarray": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
+ "integrity": "sha1-wCUTUV4wna3dTCTGDP3c9ZdtkRU="
+ },
+ "lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
+ },
+ "lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
+ },
+ "merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
+ },
+ "micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "requires": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ }
+ },
+ "mini-svg-data-uri": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
+ "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="
+ },
+ "minimist": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+ },
+ "nanoid": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.2.tgz",
+ "integrity": "sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA=="
+ },
+ "node-releases": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz",
+ "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg=="
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
+ },
+ "normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI="
+ },
+ "object-hash": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
+ "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="
+ },
+ "parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "requires": {
+ "callsites": "^3.0.0"
+ }
+ },
+ "parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ }
+ },
+ "path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+ },
+ "path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
+ },
+ "picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+ },
+ "picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
+ },
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true
+ },
+ "postcss": {
+ "version": "8.4.12",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.12.tgz",
+ "integrity": "sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==",
+ "requires": {
+ "nanoid": "^3.3.1",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "postcss-import": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz",
+ "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==",
+ "dev": true,
+ "requires": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ }
+ },
+ "postcss-js": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz",
+ "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==",
+ "requires": {
+ "camelcase-css": "^2.0.1"
+ }
+ },
+ "postcss-load-config": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
+ "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
+ "requires": {
+ "lilconfig": "^2.0.5",
+ "yaml": "^1.10.2"
+ }
+ },
+ "postcss-nested": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz",
+ "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==",
+ "requires": {
+ "postcss-selector-parser": "^6.0.6"
+ }
+ },
+ "postcss-selector-parser": {
+ "version": "6.0.10",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
+ "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
+ "requires": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ }
+ },
+ "postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
+ },
+ "queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
+ },
+ "quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
+ },
+ "read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=",
+ "dev": true,
+ "requires": {
+ "pify": "^2.3.0"
+ }
+ },
+ "readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+ "dev": true
+ },
+ "resolve": {
+ "version": "1.22.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
+ "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
+ "requires": {
+ "is-core-module": "^2.8.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ }
+ },
+ "resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
+ },
+ "reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="
+ },
+ "robust-predicates": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.1.tgz",
+ "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==",
+ "dev": true
+ },
+ "run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "requires": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=",
+ "dev": true
+ },
+ "rxjs": {
+ "version": "6.6.7",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
+ "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
+ },
+ "spawn-command": {
+ "version": "0.0.2-1",
+ "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
+ "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
+ },
+ "tailwindcss": {
+ "version": "3.0.23",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.23.tgz",
+ "integrity": "sha512-+OZOV9ubyQ6oI2BXEhzw4HrqvgcARY38xv3zKcjnWtMIZstEsXdI9xftd1iB7+RbOnj2HOEzkA0OyB5BaSxPQA==",
+ "requires": {
+ "arg": "^5.0.1",
+ "chalk": "^4.1.2",
+ "chokidar": "^3.5.3",
+ "color-name": "^1.1.4",
+ "cosmiconfig": "^7.0.1",
+ "detective": "^5.2.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.2.11",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^2.2.0",
+ "postcss": "^8.4.6",
+ "postcss-js": "^4.0.0",
+ "postcss-load-config": "^3.1.0",
+ "postcss-nested": "5.0.6",
+ "postcss-selector-parser": "^6.0.9",
+ "postcss-value-parser": "^4.2.0",
+ "quick-lru": "^5.1.1",
+ "resolve": "^1.22.0"
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true
+ },
+ "tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+ },
+ "wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
+ },
+ "y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true
+ },
+ "yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
+ },
+ "yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "dev": true,
+ "requires": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ }
+ },
+ "yargs-parser": {
+ "version": "20.2.9",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+ "dev": true
+ }
+ }
+}
diff --git a/apps/dreki_web/assets/package.json b/apps/dreki_web/assets/package.json
new file mode 100644
index 0000000..07072c5
--- /dev/null
+++ b/apps/dreki_web/assets/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "dreki-assets",
+ "version": "0.0.0",
+ "description": "dreki frontend files",
+ "scripts": {
+ "deploy-css": "NODE_ENV=production tailwindcss --postcss --minify -i app.css -o ../priv/static/app.css",
+ "watch-css": "NODE_ENV=dev tailwindcss --input=app.css --output=../priv/static/app.css --postcss --watch",
+ "deploy-js": "NODE_ENV=production esbuild app.js --bundle --minify --target=es2016 --outfile=../priv/static/app.js",
+ "watch-js": "NODE_ENV=dev esbuild app.js --bundle --sourcemap=inline --watch --outfile=../priv/static/app.js",
+ "watch": "NODE_ENV=dev concurrently npm:watch-css npm:watch-js",
+ "deploy": "NODE_ENV=dev concurrently npm:deploy-css npm:watch-css"
+ },
+ "devDependencies": {
+ "@hotwired/stimulus": "^3.0.1",
+ "@hotwired/turbo": "^7.1.0",
+ "autoprefixer": "^10.4.4",
+ "concurrently": "^6.2.0",
+ "d3": "^7.4.0",
+ "d3-graphviz": "^4.1.0",
+ "esbuild": "^0.12.17",
+ "postcss": "^8.4.12",
+ "postcss-import": "^14.0.2",
+ "tailwindcss": "^3.0.23"
+ },
+ "dependencies": {
+ "@tailwindcss/aspect-ratio": "^0.4.0",
+ "@tailwindcss/forms": "^0.5.0",
+ "@tailwindcss/line-clamp": "^0.3.1",
+ "@tailwindcss/typography": "^0.5.2",
+ "crossfilter": "^1.3.12"
+ }
+}
diff --git a/apps/dreki_web/assets/postcss.config.js b/apps/dreki_web/assets/postcss.config.js
new file mode 100644
index 0000000..7626e36
--- /dev/null
+++ b/apps/dreki_web/assets/postcss.config.js
@@ -0,0 +1,8 @@
+module.exports = {
+ plugins: {
+ 'postcss-import': {},
+ 'tailwindcss/nesting': {},
+ tailwindcss: {},
+ autoprefixer: {},
+ }
+ }
diff --git a/apps/dreki_web/assets/rebar.lock b/apps/dreki_web/assets/rebar.lock
new file mode 100644
index 0000000..57afcca
--- /dev/null
+++ b/apps/dreki_web/assets/rebar.lock
@@ -0,0 +1 @@
+[].
diff --git a/apps/dreki_web/assets/tailwind.config.js b/apps/dreki_web/assets/tailwind.config.js
new file mode 100644
index 0000000..395651b
--- /dev/null
+++ b/apps/dreki_web/assets/tailwind.config.js
@@ -0,0 +1,26 @@
+const colors = require('tailwindcss/colors')
+
+module.exports = {
+ content: [
+ './app.css',
+ './app.js',
+ '../templates/*.dtl',
+ '../src/**/*.erl'
+ ],
+ theme: {
+ extend: {
+ colors: {
+ primary: colors.rose,
+ secondary: colors.stone,
+ gray: colors.stone,
+ green: colors.emerald,
+ yellow: colors.amber,
+ purple: colors.violet,
+ red: colors.rose,
+ }
+ },
+ },
+ plugins: [
+ require('@tailwindcss/typography'),
+ ],
+ }
diff --git a/apps/dreki_web/package-lock.json b/apps/dreki_web/package-lock.json
new file mode 100644
index 0000000..eb9500d
--- /dev/null
+++ b/apps/dreki_web/package-lock.json
@@ -0,0 +1,6 @@
+{
+ "name": "dreki_web",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {}
+}
diff --git a/apps/dreki_web/priv/docs_html_fragments/API.html b/apps/dreki_web/priv/docs_html_fragments/API.html
new file mode 100644
index 0000000..797c478
--- /dev/null
+++ b/apps/dreki_web/priv/docs_html_fragments/API.html
@@ -0,0 +1,13 @@
+<h2 id="api">API</h2>
+
+<h3 id="apiadmin">/api/admin/</h3>
+
+<h4 id="get-apiadminworldgraph">GET /api/admin/world/graph</h4>
+
+<ul>
+ <li>200 OK</li>
+ <li>
+ <p>401 Unauthorized</p>
+ </li>
+ <li>Alternative format: .dot</li>
+</ul>
diff --git a/apps/dreki_web/rebar.config b/apps/dreki_web/rebar.config
new file mode 100644
index 0000000..b16060a
--- /dev/null
+++ b/apps/dreki_web/rebar.config
@@ -0,0 +1,27 @@
+{erl_opts, [debug_info]}.
+{deps, [
+ {cowboy, "2.9.0"},
+ {trails, "2.3.0"},
+ {erlydtl, "0.14.0"},
+ {oauth2c, {git, "https://github.com/kivra/oauth2_client", {branch, "master"}}},
+ {cowboy_telemetry, "~> 0.4.0"},
+ {opentelemetry_cowboy, "~> 0.1.0"}
+]}.
+
+{plugins, [
+ {rebar3_erlydtl_plugin, ".*", {git, "https://github.com/tsloughter/rebar3_erlydtl_plugin.git", {branch, "master"}}}
+]}.
+
+{provider_hooks, [
+ {pre, [{compile, {erlydtl, compile}}]}
+ ]}.
+
+{shell, [
+ % {config, "config/sys.config"},
+ {apps, [dreki_web]}
+]}.
+
+{erlydtl_opts, [
+ {doc_root, "templates"},
+ {custom_tags_dir, "templates/tags"}
+]}.
diff --git a/apps/dreki_web/rebar.lock b/apps/dreki_web/rebar.lock
new file mode 100644
index 0000000..28cda2d
--- /dev/null
+++ b/apps/dreki_web/rebar.lock
@@ -0,0 +1,65 @@
+{"1.2.0",
+[{<<"base64url">>,{pkg,<<"base64url">>,<<"0.0.1">>},2},
+ {<<"certifi">>,{pkg,<<"certifi">>,<<"2.9.0">>},3},
+ {<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.9.0">>},0},
+ {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.11.0">>},1},
+ {<<"erlsom">>,{pkg,<<"erlsom">>,<<"1.5.0">>},2},
+ {<<"erlydtl">>,{pkg,<<"erlydtl">>,<<"0.14.0">>},0},
+ {<<"hackney">>,{pkg,<<"hackney">>,<<"1.17.0">>},2},
+ {<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},3},
+ {<<"jose">>,
+ {git,"https://github.com/potatosalad/erlang-jose.git",
+ {ref,"090a2ed054304ecc012d6c2d9d10d2a294d835b1"}},
+ 1},
+ {<<"jsx">>,{pkg,<<"jsx">>,<<"2.9.0">>},2},
+ {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},3},
+ {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},3},
+ {<<"oauth2c">>,
+ {git,"https://github.com/kivra/oauth2_client",
+ {ref,"47ba50a57e981253246b732cf31fb4df83fa260f"}},
+ 0},
+ {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.4.1">>},3},
+ {<<"ranch">>,{pkg,<<"ranch">>,<<"1.8.0">>},1},
+ {<<"restc">>,
+ {git,"https://github.com/kivra/restclient.git",
+ {ref,"993fdb6005da69252ec98573b2c4ea5a905011ff"}},
+ 1},
+ {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.6">>},3},
+ {<<"trails">>,{pkg,<<"trails">>,<<"2.3.0">>},0},
+ {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},3}]}.
+[
+{pkg_hash,[
+ {<<"base64url">>, <<"36A90125F5948E3AFD7BE97662A1504B934DD5DAC78451CA6E9ABF85A10286BE">>},
+ {<<"certifi">>, <<"6F2A475689DD47F19FB74334859D460A2DC4E3252A3324BD2111B8F0429E7E21">>},
+ {<<"cowboy">>, <<"865DD8B6607E14CF03282E10E934023A1BD8BE6F6BACF921A7E2A96D800CD452">>},
+ {<<"cowlib">>, <<"0B9FF9C346629256C42EBE1EEB769A83C6CB771A6EE5960BD110AB0B9B872063">>},
+ {<<"erlsom">>, <<"C5A5CDD0EE0E8DCA62BCC4B13FF08DA24FDEFC16CCD8B25282A2FDA2BA1BE24A">>},
+ {<<"erlydtl">>, <<"964B2DC84F8C17ACFAA69C59BA129EF26AC45D2BA898C3C6AD9B5BDC8BA13CED">>},
+ {<<"hackney">>, <<"717EA195FD2F898D9FE9F1CE0AFCC2621A41ECFE137FAE57E7FE6E9484B9AA99">>},
+ {<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>},
+ {<<"jsx">>, <<"D2F6E5F069C00266CAD52FB15D87C428579EA4D7D73A33669E12679E203329DD">>},
+ {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>},
+ {<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>},
+ {<<"parse_trans">>, <<"6E6AA8167CB44CC8F39441D05193BE6E6F4E7C2946CB2759F015F8C56B76E5FF">>},
+ {<<"ranch">>, <<"8C7A100A139FD57F17327B6413E4167AC559FBC04CA7448E9BE9057311597A1D">>},
+ {<<"ssl_verify_fun">>, <<"CF344F5692C82D2CD7554F5EC8FD961548D4FD09E7D22F5B62482E5AEAEBD4B0">>},
+ {<<"trails">>, <<"B09703F056705F4943E14FFF077B98C711A6F48FAD40F4FF0B350794074AD69C">>},
+ {<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>}]},
+{pkg_hash_ext,[
+ {<<"base64url">>, <<"FAB09B20E3F5DB886725544CBCF875B8E73EC93363954EB8A1A9ED834AA8C1F9">>},
+ {<<"certifi">>, <<"266DA46BDB06D6C6D35FDE799BCB28D36D985D424AD7C08B5BB48F5B5CDD4641">>},
+ {<<"cowboy">>, <<"2C729F934B4E1AA149AFF882F57C6372C15399A20D54F65C8D67BEF583021BDE">>},
+ {<<"cowlib">>, <<"2B3E9DA0B21C4565751A6D4901C20D1B4CC25CBB7FD50D91D2AB6DD287BC86A9">>},
+ {<<"erlsom">>, <<"55A9DBF9CFA77FCFC108BD8E2C4F9F784DEA228A8F4B06EA10B684944946955A">>},
+ {<<"erlydtl">>, <<"D80EC044CD8F58809C19D29AC5605BE09E955040911B644505E31E9DD8143431">>},
+ {<<"hackney">>, <<"64C22225F1EA8855F584720C0E5B3CD14095703AF1C9FBC845BA042811DC671C">>},
+ {<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>},
+ {<<"jsx">>, <<"8EE1DB1CABAFDD578A2776A6AAAE87C2A8CE54B47B59E9EC7DAB5D7EB71CD8DC">>},
+ {<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>},
+ {<<"mimerl">>, <<"F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323">>},
+ {<<"parse_trans">>, <<"620A406CE75DADA827B82E453C19CF06776BE266F5A67CFF34E1EF2CBB60E49A">>},
+ {<<"ranch">>, <<"49FBCFD3682FAB1F5D109351B61257676DA1A2FDBE295904176D5E521A2DDFE5">>},
+ {<<"ssl_verify_fun">>, <<"BDB0D2471F453C88FF3908E7686F86F9BE327D065CC1EC16FA4540197EA04680">>},
+ {<<"trails">>, <<"40804001EB80417AA9D02400F39B7216956C3F251539A8A6096A69B3FAC0EA07">>},
+ {<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>}]}
+].
diff --git a/apps/dreki_web/src/dreki_web.app.src b/apps/dreki_web/src/dreki_web.app.src
new file mode 100644
index 0000000..cf731fc
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web.app.src
@@ -0,0 +1,20 @@
+{application, dreki_web,
+ [{description, "An OTP application"},
+ {vsn, "0.1.0"},
+ {registered, []},
+ {mod, {dreki_web_app, []}},
+ {applications,
+ [kernel,
+ stdlib,
+ erlydtl,
+ cowboy,
+ trails,
+ cowboy_telemetry,
+ opentelemetry_cowboy
+ ]},
+ {env,[]},
+ {modules, []},
+
+ {licenses, ["Apache 2.0"]},
+ {links, []}
+ ]}.
diff --git a/apps/dreki_web/src/dreki_web.erl b/apps/dreki_web/src/dreki_web.erl
new file mode 100644
index 0000000..f9e01e6
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web.erl
@@ -0,0 +1,71 @@
+-module(dreki_web).
+
+-export([reply/5, reply_json/3, reply_json/4, temporary_redirect/2, req_param/2, identity_name/1]).
+-export([content_types_accepted/2, content_types_provided/2]).
+-export([detect_web_mimetype/1]).
+
+identity_name(#{identity := Identity}) ->
+ identity_name(Identity);
+identity_name(#{<<"identity">> := Identity}) ->
+ identity_name(Identity);
+identity_name(#{<<"traits">> := #{<<"name">> := #{<<"first">> := F, <<"last">> := L}}}) when is_binary(F), is_binary(L) ->
+ [F, " ", L];
+identity_name(#{<<"traits">> := #{<<"name">> := N}}) when is_binary(N) ->
+ N;
+identity_name(#{<<"traits">> := #{<<"username">> := U}}) when is_binary(U) ->
+ U;
+identity_name(#{<<"traits">> := #{<<"email">> := E}}) when is_binary(E) ->
+ E;
+identity_name(#{<<"id">> := Id}) ->
+ Id.
+
+reply(Req, Code, Json, Headers, json) ->
+ reply_json(Req, Code, Json, Headers);
+reply(Req, Code, Json, Headers, yaml) ->
+ reply_yaml(Req, Code, Json, Headers).
+
+reply_json(Req, Code, Json) ->
+ reply_json(Req, Code, Json, #{}).
+reply_json(Req, Code, Json, Headers0) when is_binary(Json) ->
+ Headers = maps:put(<<"content-type">>, <<"application/json">>, Headers0),
+ cowboy_req:reply(Code, Headers, Json, Req);
+reply_json(Req, Code, Json, Headers0) ->
+ reply_json(Req, Code, jsone:encode(Json), Headers0).
+
+reply_yaml(Req, Code, Yaml) ->
+ reply_yaml(Req, Code, Yaml, #{}).
+reply_yaml(Req, Code, Yaml, Headers0) when is_binary(Yaml) ->
+ Headers = maps:put(<<"content-type">>, <<"application/yaml">>, Headers0),
+ cowboy_req:reply(Code, Headers, Yaml, Req);
+reply_yaml(Req, Code, Yaml, Headers0) ->
+ reply_yaml(Req, Code, fast_yaml:encode(Yaml), Headers0).
+
+temporary_redirect(Req0, Url) ->
+ cowboy_req:reply(307, #{<<"location">> => Url}, Req0).
+
+req_param(Req, Param) ->
+ Qs = cowboy_req:parse_qs(Req),
+ case lists:keyfind(Param, 1, Qs) of
+ {_, Value} ->
+ {ok, Value};
+ _ ->
+ {error, {missing_param, Param}}
+ end.
+
+content_types_accepted(Req, State) ->
+ {[
+ {{ <<"application">>, <<"json">>, '*'}, from_json},
+ {{ <<"application">>, <<"yaml">>, '*'}, from_yaml},
+ {{ <<"multipart">>, <<"form-data">>, '*'}, from_form}
+ ], Req, State}.
+
+content_types_provided(Req, State) ->
+ {[{{ <<"application">>, <<"json">>, '*'}, to_json}], Req, State}.
+
+detect_web_mimetype(Path) when is_binary(Path) ->
+ detect_web_mimetype(lists:reverse(binary_to_list(Path)), Path).
+
+detect_web_mimetype([$m, $s, $a, $w, $. | _], _) ->
+ {<<"application">>, <<"wasm">>, []};
+detect_web_mimetype(P, Path) ->
+ cow_mimetypes:web(Path).
diff --git a/apps/dreki_web/src/dreki_web_admin_tasks.erl b/apps/dreki_web/src/dreki_web_admin_tasks.erl
new file mode 100644
index 0000000..adc2689
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web_admin_tasks.erl
@@ -0,0 +1,30 @@
+-module(dreki_web_admin_tasks).
+-behaviour(cowboy_rest).
+-export([init/2]).
+-export([allowed_methods/2]).
+-export([content_types_accepted/2]).
+-export([content_types_provided/2]).
+
+init(Req, State) ->
+ {cowboy_rest, Req, State}.
+
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"HEAD">>, <<"OPTIONS">>, <<"POST">>], Req, State}.
+
+content_types_accepted(Req, State) ->
+ dreki_web:content_types_accepted(Req, State).
+
+content_types_provided(Req, State) ->
+ dreki_web:content_types_provided(Req, State).
+
+list(Req, State, Format) ->
+ {ok, Db} = dreki_tasks:open(),
+ {ok, Tasks} = dreki_tasks:all(Db),
+ MTasks = lists:foldl(fun (T, Acc) -> [dreki_task:to_map(T) | Acc] end, [], Tasks),
+ dreki_web:reply(Req, 200, #{error => false, tasks => MTasks}, [], Format).
+
+to_json(Req, State) ->
+ list(Req, State, json).
+
+to_yaml(Req, State) ->
+ list(Req, State, yaml).
diff --git a/apps/dreki_web/src/dreki_web_admin_world.erl b/apps/dreki_web/src/dreki_web_admin_world.erl
new file mode 100644
index 0000000..1ceaaee
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web_admin_world.erl
@@ -0,0 +1,28 @@
+-module(dreki_web_admin_world).
+-behaviour(cowboy_handler).
+-export([init/2]).
+
+init(Req, index) ->
+ Json = #{<<"data">> => #{<<"service">> => <<"dreki">>}},
+ {ok, dreki_web:reply_json(Req, 200, Json), undefined};
+
+init(Req, graph) ->
+ World = dreki_world_dns:as_map(),
+ Json = #{<<"data">> => World},
+ {ok, dreki_web:reply_json(Req, 200, Json), undefined};
+
+init(Req, {graph, dot}) ->
+ World = dreki_world_dns:as_map(),
+ Vertices = [dot_format_vertex(N) || N <- maps:get(vertices, World)],
+ {ok, Dot} = world_graph_dot_dtl:render([{vertices, Vertices}, {edges, maps:get(edges, World)}]),
+ cowboy_req:reply(200, #{<<"content-type">> => <<"text/vnd.graphviz">>}, Dot, Req);
+
+init(Req, _) ->
+ dreki_web_error:init(Req, #{code => 404, status => "Not found"}).
+
+dot_format_vertex(V = #{type := node}) ->
+ V#{shape => <<"box">>, color => <<"#c2410c">>, class => <<"dreki-world-graph-node">>};
+dot_format_vertex(V = #{type := root}) ->
+ V#{shape => <<"polygon">>, color => <<"#a16207">>, class => <<"dreki-world-graph-root">>};
+dot_format_vertex(V = #{type := region}) ->
+ V#{shape => <<"egg">>, color => <<"#4d7c0f">>, class => <<"dreki-world-graph-region">>}.
diff --git a/apps/dreki_web/src/dreki_web_app.erl b/apps/dreki_web/src/dreki_web_app.erl
new file mode 100644
index 0000000..5b6454e
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web_app.erl
@@ -0,0 +1,68 @@
+%%%-------------------------------------------------------------------
+%% @doc dreki_web public API
+%% @end
+%%%-------------------------------------------------------------------
+
+-module(dreki_web_app).
+
+-behaviour(application).
+
+-export([start/2, stop/1]).
+
+start(_StartType, _StartArgs) ->
+ Config = application:get_all_env(dreki_web),
+ Transport = proplists:get_value(transport, Config),
+ CowboyEnv = #{
+ middlewares => [dreki_web_auth, cowboy_router, cowboy_handler],
+ stream_handlers => [cowboy_telemetry_h, cowboy_stream_h],
+ env => #{
+ dispatch => routes()
+ }
+ },
+ {ok, _} = cowboy:start_clear(dreki_web_listener, Transport, CowboyEnv),
+ opentelemetry_cowboy:setup(),
+ IP = proplists:get_value(ip, Transport),
+ Port = proplists:get_value(port, Transport),
+ logger:notice("dreki_web listening on ~p:~p", [IP, Port]),
+ dreki_web_sup:start_link().
+
+stop(_State) ->
+ ok.
+
+%% internal functions
+
+routes() ->
+ Trails = [
+ {"/", dreki_web_index, undefined},
+ {"/static/[...]", cowboy_static,
+ {priv_dir, dreki_web, "static", [{mimetypes, dreki_web, detect_web_mimetype}]}},
+
+ %% API
+ {"/api/tasks/:id", dreki_web_task, undefined},
+
+ %% Admin API
+ {"/api/admin/world", dreki_web_admin_world, index},
+ {"/api/admin/world/graph", dreki_web_admin_world, graph},
+ {"/api/admin/world/graph.dot", dreki_web_admin_world, {graph, dot}},
+ {"/api/admin/tasks", dreki_web_admin_tasks, undefined},
+ {"/api/admin/tasks/:id", dreki_web_admin_task, undefined},
+
+ %% Admin UI
+ {"/admin", dreki_web_ui_index, undefined},
+ {"/admin/nodes/:id", dreki_web_ui_node, undefined},
+ {"/admin/tasks", dreki_web_ui_tasks, undefined},
+ {"/admin/tasks/:id", dreki_web_ui_task, undefined},
+
+ %%{"/admin/stores", dreki_web_ui_stores, undefined},
+ %%{"/admin/:location/stores", dreki_web_ui_stores, undefined},
+ {"/admin/:location/:namespace", dreki_web_ui_stores, undefined},
+ {"/admin/:location/:namespace/:directory", dreki_web_ui_stores, undefined},
+ {"/admin/:location/:namespace/:directory/_/:action", dreki_web_ui_stores, action},
+ {"/admin/:location/:namespace/:directory/:id", dreki_web_ui_stores, undefined},
+
+ {"/admin/[...]", dreki_web_ui_error, #{code => 404, status => <<"Not found">>}},
+
+ %% 404 Catch all
+ {'_', dreki_web_error, #{code => 404, status => <<"Not found">>}}
+ ],
+ trails:single_host_compile(Trails).
diff --git a/apps/dreki_web/src/dreki_web_auth.erl b/apps/dreki_web/src/dreki_web_auth.erl
new file mode 100644
index 0000000..48b0609
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web_auth.erl
@@ -0,0 +1,45 @@
+-module(dreki_web_auth).
+-behaviour(cowboy_middleware).
+-export([execute/2]).
+
+execute(Req0, Env) ->
+ Header = cowboy_req:header(<<"authorization">>, Req0),
+ Cookie = cowboy_req:header(<<"cookie">>, Req0),
+ case {Header, Cookie} of
+ {<<"Basic ", Basic/binary>>, _} -> basic_oauth2_client_credentials(Basic, Req0, Env);
+ {_, Cookie} when is_binary(Cookie) -> cookie(Cookie, Req0, Env);
+ _ -> error(Req0, 401, <<"Unauthorized">>, <<"Unauthorized">>, Env)
+ end.
+
+basic_oauth2_client_credentials(Base64, Req0, Env) ->
+ Binary = base64:decode(Base64),
+ [User, Pass] = binary:split(Binary, <<":">>),
+ oauth2_client_credentials(User, Pass, Req0, Env).
+
+oauth2_client_credentials(User, Pass, Req0, Env) ->
+ logger:debug("Trying with oauth2 ~p ~p", [User, Pass]),
+ Url = [ory_hydra:url(), "/oauth2/token"],
+ case oauth2c:retrieve_access_token(<<"client_credentials">>, Url, User, Pass) of
+ {ok, Headers, Client} ->
+ logger:debug("Headers ~p Client ~p", [Headers, Client]),
+ identity(Req0, Headers, Env)
+ end.
+
+cookie(Cookie, Req0, Env) ->
+ case ory_kratos:whoami(Cookie) of
+ {ok, Session = #{<<"active">> := true, <<"identity">> := Identity}} ->
+ identity(Req0, Session, Env);
+ {error, #{<<"code">> := Code, <<"status">> := Status, <<"message">> := Msg}} ->
+ error(Req0, Code, Status, Msg, Env)
+ end.
+
+identity(Req0, Identity, Env0) ->
+ IdentityId = maps:get(<<"id">>, Identity),
+ Env1 = maps:put(<<"identity">>, Identity, Env0),
+ Env2 = maps:put(<<"identity_id">>, IdentityId, Env1),
+ {ok, Req0#{identity => Identity, identity_id => IdentityId}, Env2}.
+
+error(Req0, Code, Status, Msg, _Env) ->
+ Json = #{<<"error">> => #{<<"code">> => Code, <<"status">> => Status, <<"message">> => Msg}},
+ Req = dreki_web:reply_json(Req0, Code, Json),
+ {stop, Req}.
diff --git a/apps/dreki_web/src/dreki_web_error.erl b/apps/dreki_web/src/dreki_web_error.erl
new file mode 100644
index 0000000..e300582
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web_error.erl
@@ -0,0 +1,20 @@
+-module(dreki_web_error).
+-behaviour(cowboy_handler).
+-export([init/2]).
+
+init(Req, not_found) ->
+ reply(Req, 404, <<"Not Found">>, undefined);
+init(Req, State = #{code := Code, status := Status}) ->
+ reply(Req, Code, Status, maps:get(message, State, undefined));
+init(Req, oauth2) ->
+ {ok, ErrorDescription} = dreki_web:req_param(Req, <<"error_description">>),
+ reply(Req, 500, <<"Error">>, ErrorDescription);
+init(Req = #{method := <<"GET">>}, _) ->
+ {ok, ErrorId} = dreki_web:req_param(Req, <<"id">>),
+ {ok, #{<<"error">> := #{<<"status">> := Status, <<"code">> := Code, <<"message">> := Msg}}} = ory_kratos:error(ErrorId),
+ reply(Req, Code, Status, Msg).
+
+reply(Req0, Code, Status, Msg) ->
+ Json = #{<<"error">> => #{<<"code">> => Code, <<"status">> => Status, <<"message">> => Msg}},
+ Req = dreki_web:reply_json(Req0, Code, Json),
+ {ok, Req, undefined}.
diff --git a/apps/dreki_web/src/dreki_web_index.erl b/apps/dreki_web/src/dreki_web_index.erl
new file mode 100644
index 0000000..2ed8a38
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web_index.erl
@@ -0,0 +1,9 @@
+-module(dreki_web_index).
+-behaviour(cowboy_handler).
+-export([init/2]).
+
+init(Req = #{method := <<"GET">>}, _) ->
+ Json = #{<<"error">> => false, <<"service">> => <<"dreki">>},
+ {ok, dreki_web:reply_json(Req, 200, Json), undefined};
+init(Req, _) ->
+ dreki_web_error:init(Req, #{code => 400, status => "Bad request"}).
diff --git a/apps/dreki_web/src/dreki_web_sup.erl b/apps/dreki_web/src/dreki_web_sup.erl
new file mode 100644
index 0000000..8414aa7
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web_sup.erl
@@ -0,0 +1,35 @@
+%%%-------------------------------------------------------------------
+%% @doc dreki_web top level supervisor.
+%% @end
+%%%-------------------------------------------------------------------
+
+-module(dreki_web_sup).
+
+-behaviour(supervisor).
+
+-export([start_link/0]).
+
+-export([init/1]).
+
+-define(SERVER, ?MODULE).
+
+start_link() ->
+ supervisor:start_link({local, ?SERVER}, ?MODULE, []).
+
+%% sup_flags() = #{strategy => strategy(), % optional
+%% intensity => non_neg_integer(), % optional
+%% period => pos_integer()} % optional
+%% child_spec() = #{id => child_id(), % mandatory
+%% start => mfargs(), % mandatory
+%% restart => restart(), % optional
+%% shutdown => shutdown(), % optional
+%% type => worker(), % optional
+%% modules => modules()} % optional
+init([]) ->
+ SupFlags = #{strategy => one_for_all,
+ intensity => 0,
+ period => 1},
+ ChildSpecs = [],
+ {ok, {SupFlags, ChildSpecs}}.
+
+%% internal functions
diff --git a/apps/dreki_web/src/dreki_web_task.erl b/apps/dreki_web/src/dreki_web_task.erl
new file mode 100644
index 0000000..fdcf9cd
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web_task.erl
@@ -0,0 +1,21 @@
+-module(dreki_web_task).
+-behaviour(cowboy_handler).
+-behaviour(cowboy_rest).
+-export([init/2]).
+-export([allowed_methods/2]).
+-export([content_types_accepted/2]).
+-export([content_types_provided/2]).
+
+init(Req, State) ->
+ {cowboy_rest, Req, State}.
+
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"HEAD">>, <<"OPTIONS">>, <<"POST">>], Req, State}.
+
+content_types_accepted(Req, State) ->
+ dreki_web:content_types_accepted(Req, State).
+
+content_types_provided(Req, State) ->
+ dreki_web:content_types_provided(Req, State).
+
+
diff --git a/apps/dreki_web/src/dreki_web_ui.erl b/apps/dreki_web/src/dreki_web_ui.erl
new file mode 100644
index 0000000..d60cc6e
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web_ui.erl
@@ -0,0 +1,43 @@
+-module(dreki_web_ui).
+-export([render/3, reply_html/3, reply_html/4]).
+
+render(Req, InnerModule, Assigns0) ->
+ Assigns = assigns(Req, Assigns0),
+ {ok, InnerHtml} = InnerModule:render(Assigns),
+ render_layout(Req, InnerHtml, Assigns).
+
+reply_html(Req, Code, Html) ->
+ reply_html(Req, Code, Html, #{}).
+
+reply_html(Req, Code, Html, Headers0) ->
+ Headers = maps:put(<<"content-type">>, <<"text/html">>, Headers0),
+ cowboy_req:reply(Code, Headers, Html, Req).
+
+render_layout(Req, InnerHtml, Assigns) ->
+ {ok, Html} = layout_dtl:render([{"inner", InnerHtml} | Assigns]),
+ Html.
+
+assigns(Req, Assigns0) ->
+ Assigns = clean_assigns(Assigns0),
+ [{"site_title", "Dreki"},
+ {"identity_id", maps:get(identity_id, Req)},
+ {"identity", maps:get(identity, Req)},
+ {"identity_name", dreki_web:identity_name(Req)},
+ {"dreki_node", node()},
+ {"dreki_world", dreki_world:to_map()}
+ | Assigns].
+
+content_types_accepted(Req, State) ->
+ {[
+ {{ <<"multipart">>, <<"form-data">>, '*'}, from_form}
+ ], Req, State}.
+
+content_types_provided(Req, State) ->
+ {[{{ <<"text">>, <<"html">>, '*'}, to_html}], Req, State}.
+
+clean_assigns(Assigns) when is_list(Assigns) ->
+ Assigns;
+clean_assigns(Map) when is_map(Map) ->
+ maps:fold(fun (Key, Value, Acc) -> [{Key, clean_assigns(Value)} | Acc] end, [], Map);
+clean_assigns(Other) ->
+ Other.
diff --git a/apps/dreki_web/src/dreki_web_ui_error.erl b/apps/dreki_web/src/dreki_web_ui_error.erl
new file mode 100644
index 0000000..ccda150
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web_ui_error.erl
@@ -0,0 +1,14 @@
+-module(dreki_web_ui_error).
+-behaviour(cowboy_handler).
+-export([init/2]).
+
+init(Req, not_found) ->
+ reply(Req, 404, <<"Not Found">>, undefined);
+init(Req, State = #{code := Code, status := Status}) ->
+ reply(Req, Code, Status, maps:get(message, State, undefined)).
+
+reply(Req0, Code, Status, Msg) ->
+ Assigns = [{"message", Msg}, {"status", Status}],
+ Html = dreki_web_ui:render(Req0, error_dtl, Assigns),
+ Req = dreki_web_ui:reply_html(Req0, Code, Html),
+ {ok, Req, undefined}.
diff --git a/apps/dreki_web/src/dreki_web_ui_index.erl b/apps/dreki_web/src/dreki_web_ui_index.erl
new file mode 100644
index 0000000..9f4684e
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web_ui_index.erl
@@ -0,0 +1,31 @@
+-module(dreki_web_ui_index).
+-behaviour(cowboy_handler).
+-export([init/2]).
+
+init(Req = #{method := <<"GET">>}, State) ->
+ PrettyWorld = jsone:encode(dreki_world:to_map(), [canonical_form, {space, 1}, {indent, 4}]),
+ LocalTasksStores = dreki_tasks:local_stores(),
+ {ok, Peers} = partisan_peer_service:members(),
+ NavTree = nav_tree(dreki_world_dns:as_map()),
+ Html = dreki_web_ui:render(Req, index_dtl, [
+ {"page_title", "Admin UI"},
+ {"dreki_world_pretty_json", PrettyWorld},
+ {"dreki_local_tasks_stores", LocalTasksStores},
+ {"dreki_peers", Peers},
+ {"nav_tree", NavTree}
+ ]),
+ {ok, dreki_web_ui:reply_html(Req, 200, Html), State};
+init(Req, _) ->
+ dreki_web_ui_error:init(Req, #{code => 400, status => "Bad request"}).
+
+nav_tree(#{vertices := Vertices}) ->
+ nav_tree(Vertices, []).
+
+nav_tree([Region = #{type := region, name := Name, data := #{display_name := Display}} | Rest], Acc) ->
+ nav_tree(Rest, [Region#{href => <<"/admin/regions/", Name/binary>>, title => Display} | Acc]);
+nav_tree([Node = #{type := node, name := Name, data := #{display_name := Display}} | Rest], Acc) ->
+ nav_tree(Rest, [Node#{href => <<"/admin/nodes/", Name/binary>>, title => Display} | Acc]);
+nav_tree([#{type := Root} | Rest], Acc) ->
+ nav_tree(Rest, Acc);
+nav_tree([], Acc) ->
+ Acc.
diff --git a/apps/dreki_web/src/dreki_web_ui_json_form.erl b/apps/dreki_web/src/dreki_web_ui_json_form.erl
new file mode 100644
index 0000000..49f69f8
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web_ui_json_form.erl
@@ -0,0 +1,127 @@
+-module(dreki_web_ui_json_form).
+-export([render_html/2]).
+-export([render/2]).
+-export([to_html/1]).
+
+-type dreki_form() :: #{
+ input => binary(),
+ label => binary()
+}.
+
+render_html(Schema, Opts) ->
+ Abstract = render(Schema, Opts),
+ to_html(Abstract).
+
+to_html(Atom) when is_atom(Atom) ->
+ as_binary(Atom);
+to_html(Binary) when is_binary(Binary) ->
+ Binary;
+to_html(Abstract) ->
+ to_html(Abstract, []).
+
+to_html([{Node, Attrs} | Rest], Acc) ->
+ BNode = as_binary(Node),
+ AttrsS = attrs_to_html(Attrs),
+ to_html(Rest, [[<<"<">>, BNode, <<" ">>, AttrsS, <<">">>] | Acc]);
+to_html([{Node, Attrs, Content} | Rest], Acc) ->
+ BNode = as_binary(Node),
+ AttrsS = attrs_to_html(Attrs),
+ logger:debug("Node ~p Attrs ~p Content ~p", [Node, Attrs, Content]),
+ to_html(Rest, [[<<"<">>, BNode, <<" ">>, AttrsS, <<">">>, to_html(Content), <<"</">>, BNode, <<">">>] | Acc]);
+to_html([], Acc) ->
+ lists:reverse(Acc).
+
+attrs_to_html(Attrs) -> attrs_to_html(Attrs, []).
+attrs_to_html([{Attr, undefined} | Rest], Acc) ->
+ attrs_to_html(Rest, Acc);
+attrs_to_html([{Attr, false} | Rest], Acc) ->
+ attrs_to_html(Rest, Acc);
+attrs_to_html([{Attr, true} | Rest], Acc) ->
+ attrs_to_html(Rest, [[as_binary(Attr), <<" ">>] | Acc]);
+attrs_to_html([{Attr, Num} | Rest], Acc) when is_integer(Num) ->
+ attrs_to_html(Rest, [[as_binary(Attr), <<"=">>, Num, <<" ">>] | Acc]);
+attrs_to_html([{Attr, Value} | Rest], Acc) ->
+ attrs_to_html(Rest, [[as_binary(Attr), <<"=">>, <<"\"">>, as_binary(Value), <<"\"">>, <<" ">>] | Acc]);
+attrs_to_html([], Acc) ->
+ Acc.
+
+render(Schema, Opts) ->
+ lists:reverse(maps:fold(fun (Key, Value, Acc) ->
+ case render_property(Key, Value, Schema, Schema, Opts) of
+ {ok, Html} -> [Html | Acc];
+ ignore -> Acc
+ end
+ end, [], maps:get(properties, Schema))).
+
+render_property(Field, Config = #{enum := Enum}, Parent, Schema, Opts) ->
+ FormAttrs = maps:get(<<"dreki:form">>, Config, #{}),
+ Required = lists:member(Field, maps:get(required, Parent, [])),
+ Label = maps:get(label, FormAttrs, maps:get(title, Config, Field)),
+ InputOpts = #{required => Required, label => Label, values => Enum},
+ {ok, render_input(select, Field, undefined, InputOpts, Opts)};
+
+render_property(Field, Config = #{type := Type}, Parent, Schema, Opts) ->
+ FormAttrs = maps:get(<<"dreki:form">>, Config, #{}),
+ Input = maps:get(input, FormAttrs, input),
+ InputType = maps:get(input_type, FormAttrs, input_type(Type)),
+ Required = lists:member(Field, maps:get(required, Parent, [])),
+ Label = maps:get(label, FormAttrs, maps:get(title, Config, Field)),
+ InputOpts = #{required => Required, label => Label, input_type => InputType},
+ {ok, render_input(Input, Field, Type, InputOpts, Opts)};
+
+render_property(Field, #{'$ref' := Ref}, Parent, Schema = #{'$defs' := Defs}, Opts) ->
+ case maps:get(Ref, Defs, undefined) of
+ undefined -> logger:error("didn't get ref ~p", [Ref]);
+ ExpandedRef ->
+ logger:debug("Skipping ref for now: ~p ~p", [Ref, ExpandedRef])
+ end,
+ ignore.
+
+base_attributes(Field, IOpts, FOpts) ->
+ Name = field_name(Field, FOpts),
+ {Name, [
+ {id, field_id(Field, FOpts)},
+ {name, Name},
+ {required, maps:get(required, IOpts, false)}
+ ]}.
+
+render_input(select, Field, _, IOpts, FOpts) ->
+ {Name, Attributes} = base_attributes(Field, IOpts, FOpts),
+ OptionsHtml = lists:map(fun (Opt) -> {option, [], Opt} end, maps:get(values, IOpts, [])),
+ {'div', [{class, <<"json-field">>}], [
+ {label, [{for, Name}], maps:get(label, IOpts, Field)},
+ {select, Attributes, OptionsHtml}
+ ]};
+
+render_input(input, Field, Type, IOpts, FOpts) ->
+ {Name, Attributes0} = base_attributes(Field, IOpts, FOpts),
+ Attributes = [
+ {value, maps:get(value, IOpts, undefined)},
+ {placeholder, maps:get(placeholder, IOpts, undefined)},
+ {readonly, maps:get(readonly, IOpts, false)},
+ {autocomplete, maps:get(autocomplete, IOpts, <<"off">>)},
+ {type, maps:get(input_type, IOpts)},
+ {class, <<"shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md">>}
+ | Attributes0],
+ HtmlNode = {'div', [{class, <<"json-field">>}], [
+ {label, [{for, Name}, {class, <<"block text-sm font-medium text-gray-700">>}], maps:get(label, IOpts, Field)},
+ {'div', [{class, <<"mt-1">>}], [{input, Attributes}]}
+ ]},
+ HtmlNode.
+
+as_binary(Atom) when is_atom(Atom) ->
+ atom_to_binary(Atom);
+as_binary(B) when is_binary(B) ->
+ B.
+
+input_type(_) -> <<"text">>.
+
+field_name(Field, Opts) ->
+ FB = as_binary(Field),
+ BaseName = maps:get(name, Opts, <<"form">>),
+ <<"[", BaseName/binary, "]", FB/binary>>.
+
+field_id(Field, Opts) ->
+ FB = as_binary(Field),
+ BaseId = maps:get(id, Opts, <<"form">>),
+ <<BaseId/binary, "-", FB/binary>>.
diff --git a/apps/dreki_web/src/dreki_web_ui_node.erl b/apps/dreki_web/src/dreki_web_ui_node.erl
new file mode 100644
index 0000000..fdb7c77
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web_ui_node.erl
@@ -0,0 +1,23 @@
+-module(dreki_web_ui_node).
+-behaviour(cowboy_handler).
+-behaviour(cowboy_rest).
+-export([init/2]).
+-export([allowed_methods/2]).
+-export([content_types_accepted/2]).
+-export([content_types_provided/2]).
+
+init(Req, State) ->
+ {cowboy_rest, Req, State}.
+
+allowed_methods(Req, State) ->
+ {[<<"GET">>, <<"HEAD">>, <<"OPTIONS">>, <<"POST">>], Req, State}.
+
+content_types_accepted(Req, State) ->
+ dreki_web:content_types_accepted(Req, State).
+
+content_types_provided(Req, State) ->
+ dreki_web:content_types_provided(Req, State).
+
+to_html(Req, State) ->
+ Html = dreki_web_ui:render(Req, node, []),
+ {Html, Req, State}.
diff --git a/apps/dreki_web/src/dreki_web_ui_stores.erl b/apps/dreki_web/src/dreki_web_ui_stores.erl
new file mode 100644
index 0000000..d39e571
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web_ui_stores.erl
@@ -0,0 +1,126 @@
+-module(dreki_web_ui_stores).
+-behaviour(cowboy_handler).
+-export([init/2]).
+
+
+init(Req, action) ->
+ with_location(Req#{action => cowboy_req:binding(action, Req)}, cowboy_req:binding(location, Req, undefined));
+init(Req, Action) ->
+ with_location(Req#{action => Action}, cowboy_req:binding(location, Req, undefined)).
+
+with_location(Req, undefined) ->
+ with_namespace(Req#{urn => dreki_world:root_path()});
+with_location(Req, <<"-">>) ->
+ with_namespace(Req#{urn => dreki_world:root_path()});
+with_location(Req0, Location) ->
+ Root = dreki_world:root_path(),
+ {ok, XUrn} = dreki_urn:expand(<<Root/binary, ":", Location/binary>>),
+ Req = Req0#{urn => maps:get(urn, XUrn)},
+ with_namespace(Req).
+
+with_namespace(Req) ->
+ with_namespace(Req, cowboy_req:binding(namespace, Req)).
+with_namespace(Req, undefined) ->
+ request(Req);
+with_namespace(Req = #{urn := Urn0}, Namespace) ->
+ Urn = <<Urn0/binary, "::", Namespace/binary>>,
+ with_directory(Req#{urn => Urn}).
+
+with_directory(Req) ->
+ with_directory(Req, cowboy_req:binding(directory, Req)).
+with_directory(Req, undefined) ->
+ request(Req);
+with_directory(Req = #{urn := Urn0}, Directory) ->
+ Urn = <<Urn0/binary, ":", Directory/binary>>,
+ with_id(Req#{urn => Urn}).
+
+with_id(Req) ->
+ with_id(Req, cowboy_req:binding(id, Req)).
+with_id(Req, undefined) ->
+ request(Req);
+with_id(Req = #{urn := Urn0}, Id) ->
+ Urn = <<Urn0/binary, ":", Id/binary>>,
+ request(Req#{urn => Urn}).
+
+request(Req = #{action := Action, method := Method, urn := Urn}) ->
+ logger:debug("Faked Urn ~p", [Urn]),
+ case dreki_urn:expand(Urn) of
+ {ok, XUrn} -> request(Method, Action, XUrn, Req);
+ {error, EMap=#{}} -> dreki_web_error:init(Req, EMap);
+ {error, _Error} -> dreki_web_error:init(Req, #{code => 404, status => "Not Found"})
+ end.
+
+%% Stores list
+request(<<"GET">>, undefined, #{resource := #{namespace := NS}}, Req) ->
+ {ok, Stores0} = dreki_store:stores(NS),
+ Stores = lists:map(fun (Store) ->
+ S = dreki_store:store_as_map(Store),
+ S#{href => urn_to_path(maps:get(urn, S))}
+ end, Stores0),
+ Html = dreki_web_ui:render(Req, namespace_dtl, [{namespace, NS}, {stores, Stores}]),
+ {ok, dreki_web_ui:reply_html(Req, 200, Html), undefined};
+
+%% List
+request(<<"GET">>, undefined, Urn = #{location := Loc, resource := #{directory := #{directory := Dir, namespace := NS}}}, Req) ->
+ {ok, Result} = dreki_store:list(Urn),
+ Results = lists:map(fun(Result) ->
+ Result#{href => urn_to_path(maps:get('@id', Result))}
+ end, maps:get(data, Result)),
+ Html = dreki_web_ui:render(Req, store_list_dtl, [
+ {location, Loc}, {namespace, NS}, {directory, Dir}, {results, Results},
+ {new, href(Req, <<"_/new">>)}
+ ]),
+ {ok, dreki_web_ui:reply_html(Req, 200, Html), undefined};
+
+%% New
+request(<<"GET">>, <<"new">>, #{urn := Urn, location := Loc, resource := #{directory := #{directory := Dir, namespace := NS}}}, Req) ->
+ logger:debug("Actual Urn: ~p", [Urn]),
+ {ok, Schemas} = dreki_store:list(<<Urn/binary, "::", "schemas">>),
+ {ok, Schema} = dreki_store:get(<<Urn/binary, "::", "schemas::">>),
+ Form = dreki_web_ui_json_form:render_html(Schema, #{}),
+ Html = dreki_web_ui:render(Req, store_new_dtl, [
+ {location, Loc}, {namespace, NS}, {directory, Dir},
+ {schema, Schema}, {schemas, Schemas},
+ {target, urn_to_path(Urn)}, {method, <<"POST">>},
+ {form, Form}
+ ]),
+ {ok, dreki_web_ui:reply_html(Req, 200, Html), undefined};
+
+%% Show
+request(<<"GET">>, undefined, Urn = #{location := Loc, resource := #{resource := #{id := Id, directory := Dir, namespace := NS}}}, Req) ->
+ {ok, Result0} = dreki_store:get(Urn),
+ Result = Result0#{'@href' => urn_to_path(Urn)},
+ Html = dreki_web_ui:render(Req, store_show_dtl, [{location, Loc}, {id, Id}, {directory, Dir}, {namespace, NS}, {result, Result}]),
+ {ok, dreki_web_ui:reply_html(Req, 200, Html), undefined}.
+
+derpinit(Req, _) ->
+ Json = #{<<"error">> => false, <<"service">> => <<"dreki">>},
+ logger:debug("REQ: ~p", [Req]),
+ {ok, dreki_web:reply_json(Req, 200, Json), undefined}.
+
+
+bad_request(Req) ->
+ dreki_web_error:init(Req, #{code => 400, status => "Bad request"}).
+
+href(Req = #{path := Path}, Append) ->
+ <<Path/binary, "/", Append/binary>>.
+
+location_to_path(Location) ->
+ Root = dreki_world:root_path(),
+ case Location =:= Root of
+ true -> <<"/admin/-">>;
+ false -> binary:replace(Location, <<Root/binary, ":">>, <<"/admin/">>)
+ end.
+
+urn_to_path(Urn) when is_binary(Urn) ->
+ {ok, XUrn} = dreki_urn:expand(Urn),
+ urn_to_path(XUrn);
+urn_to_path(#{location := Location, resource := #{namespace := NS}}) ->
+ LP = location_to_path(Location),
+ <<LP/binary, "/", NS/binary>>;
+urn_to_path(#{location := Location, resource := #{directory := #{directory := Dir, namespace := NS}}}) ->
+ LP = location_to_path(Location),
+ <<LP/binary, "/", NS/binary, "/", Dir/binary>>;
+urn_to_path(#{location := Location, resource := #{resource := #{id := Id, directory := Dir, namespace := NS}}}) ->
+ LP = location_to_path(Location),
+ <<LP/binary, "/", NS/binary, "/", Dir/binary, "/", Id/binary>>.
diff --git a/apps/dreki_web/src/dreki_web_ui_task.erl b/apps/dreki_web/src/dreki_web_ui_task.erl
new file mode 100644
index 0000000..1ced583
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web_ui_task.erl
@@ -0,0 +1,20 @@
+-module(dreki_web_ui_task).
+-behaviour(cowboy_handler).
+-export([init/2]).
+
+init(Req = #{method := <<"GET">>}, State) ->
+ Id = cowboy_req:binding(id, Req),
+ {ok, Db} = dreki_tasks:open(),
+ case dreki_tasks:get(Db, Id) of
+ {ok, Task} ->
+ MTask = dreki_task:to_map(Task),
+ PrettyParams = jsone:encode(maps:get(params, MTask), [canonical_form, {space, 1}, {indent, 4}]),
+ Html = dreki_web_ui:render(Req, task_dtl, [{"page_title", <<"Task: ", Id/binary>>}, {"task", MTask}, {"task_pretty_params", PrettyParams}]),
+ {ok, dreki_web_ui:reply_html(Req, 200, Html), State};
+ Error ->
+ logger:debug("Failed to lookup task ~p: ~p", [Id, Error]),
+ dreki_web_ui_error:init(Req, #{code => 404, status => "Not found"})
+ end;
+init(Req, _) ->
+ dreki_web_ui_error:init(Req, #{code => 400, status => "Bad request"}).
+
diff --git a/apps/dreki_web/src/dreki_web_ui_tasks.erl b/apps/dreki_web/src/dreki_web_ui_tasks.erl
new file mode 100644
index 0000000..e9748b8
--- /dev/null
+++ b/apps/dreki_web/src/dreki_web_ui_tasks.erl
@@ -0,0 +1,14 @@
+-module(dreki_web_ui_tasks).
+-behaviour(cowboy_handler).
+-export([init/2]).
+
+init(Req = #{method := <<"GET">>}, State) ->
+ Local = maps:fold(fun
+ (Ln, #{mod := Mod, path := Path}, Acc) ->
+ [#{name => Ln, mod => Mod, path => Path, url => <<"/api/admin/tasks/", Path>>} | Acc]
+ end, [], dreki_tasks:local_stores()),
+ Html = dreki_web_ui:render(Req, tasks_dtl, [{"page_title", "Tasks"}, {"stores", Local}, {"tasks", []}]),
+ {ok, dreki_web_ui:reply_html(Req, 200, Html), State};
+init(Req, _) ->
+ dreki_web_ui_error:init(Req, #{code => 400, status => "Bad request"}).
+
diff --git a/apps/dreki_web/templates/error.dtl b/apps/dreki_web/templates/error.dtl
new file mode 100644
index 0000000..b1ee84c
--- /dev/null
+++ b/apps/dreki_web/templates/error.dtl
@@ -0,0 +1,8 @@
+<div class="prose md:prose-lg lg:prose-xl dark:prose-invert">
+ <h2>
+ {{ status }}
+ </h2>
+ <p class="">
+ {{ message }}
+ </p>
+</div>
diff --git a/apps/dreki_web/templates/index.dtl b/apps/dreki_web/templates/index.dtl
new file mode 100644
index 0000000..8f9024f
--- /dev/null
+++ b/apps/dreki_web/templates/index.dtl
@@ -0,0 +1,38 @@
+<h1 class="text-xl">Dreki {{dreki_node}}</h1>
+
+<p>Hello, {{identity_name}}.</p>
+
+<ul class="list-disc ml-8 mx-4 my-4 nav-tree">
+ {% for el in nav_tree %}
+ <li class="{{ el.type }}"><a href="{{ el.href }}">{{ el.title }}</a></li>
+ {% endfor %}
+</ul>
+
+
+<h2 class="text-lg">Peers</h2>
+
+<ul class="list-disc ml-8 mx-4 my-4">
+ {% for peer in dreki_peers %}<li>{{ peer }}</li>{% endfor %}
+</ul>
+
+<h2 class="text-lg">World</h2>
+<div data-controller="graphviz"
+ data-graphviz-url-value="/api/admin/world/graph.dot"
+ class="px-2 py-2 mt-4">
+ <div data-graphviz-target="loading"
+ class="flex items-center justify-center">
+ <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-primary-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+ </svg>
+ Loading world graph...
+ </div>
+ <div data-graphviz-target="error" class="hidden text-red-800 dark:text-red-300">
+ Failed to load world graph
+ </div>
+ <div id="world-graph" data-graphviz-target="graph"></div>
+</div>
+
+<pre class="px-2 py-2 mt-4 bg-gray-50 dark:bg-gray-800"><code>{{ dreki_world_pretty_json }}</code></pre>
+
+<script src="/static/hpcc.wasm.min.js" type="javascript/worker"></script>
diff --git a/apps/dreki_web/templates/layout.dtl b/apps/dreki_web/templates/layout.dtl
new file mode 100644
index 0000000..b27fe30
--- /dev/null
+++ b/apps/dreki_web/templates/layout.dtl
@@ -0,0 +1,84 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>{% if page_title %}{{ page_title }} - {% endif %}{{site_title}}</title>
+ <link rel="stylesheet" href="/static/app.css">
+ <script src="/static/app.js" defer></script>
+ <link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png">
+ <link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
+ <link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
+</head>
+<body class="dark:bg-gray-900 dark:text-gray-100">
+<header class="bg-primary-600 dark:bg-primary-900">
+<div class="container mx-auto sm:px-6 lg:px-8">
+ <div class="flex-1 min-w-0 py-2">
+ <nav class="flex" aria-label="Breadcrumb">
+ <ol role="list" class="flex items-center space-x-4">
+ <li>
+ <div class="flex">
+ <a href="https://{{dreki_world.domain.internal}}" class="text-sm font-medium hover:text-white text-primary-200">{{dreki_world.domain.internal}}</a>
+ </div>
+ </li>
+{% for h in dreki_world.hiearchy %}
+ <li>
+ <div class="flex items-center">
+ <svg class="flex-shrink-0 h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
+ </svg>
+ {% if h == dreki_world.me %}
+ <a href="/admin" class="ml-4 text-sm font-medium text-primary-50 hover:text-white">{{dreki_world.me}}</a>
+ {% else %}
+ <a href="#" class="ml-4 text-sm font-medium text-primary-100 hover:text-white">{{h}}</a>
+ {% endif %}
+ </div>
+ </li>
+{% endfor %}
+ </ol>
+ </nav>
+ </div>
+
+ <nav class="" aria-label="Top">
+ <div class="w-full py-6 flex items-center justify-between border-b border-primary-500 lg:border-none">
+ <div class="flex items-center">
+ <a href="/admin" class="text-primary-100 hover:text-white text-lg">
+ <span class="sr-only">{{site_title}}</span>
+<span class="text-[2rem] inline-block">😈</span>
+<!--<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 inline-block" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
+</svg>-->
+ </a>
+ <div class="hidden ml-10 space-x-8 lg:block">
+ <a href="/admin/tasks" class="text-base font-medium text-white hover:text-primary-50"> Tasks </a>
+ </div>
+ </div>
+ <div class="ml-10 space-x-4">
+ <div class="inline-block bg-primary-500 dark:bg-primary-800 py-2 px-4 border border-transparent rounded-md text-base font-medium text-white hover:bg-opacity-75">{{identity_name}}</a>
+ </div>
+ </div>
+ <div class="py-4 flex flex-wrap justify-center space-x-6 lg:hidden">
+ <a href="/admin/tasks" class="text-base font-medium text-white hover:text-primary-50"> Tasks </a>
+ </div>
+ </nav>
+</div>
+</header>
+
+<div class="container mx-auto sm:px-6 lg:px-8 sm:py-4 lg:py-6">
+<div class="content">
+ {{ inner | safe }}
+</div>
+</div>
+
+<footer class="">
+ <div class="container mx-auto sm:px-6 lg:px-8 sm:py-4 lg:py-6">
+ <p class="mt-8 text-center text-sm text-gray-400">
+ dreki $nodename
+ </p>
+ <p class="mt-2 text-center text-xs text-gray-300">
+ identity:{{identity_id}}
+ </p>
+ </footer>
+</div>
+</body>
+</html>
diff --git a/apps/dreki_web/templates/namespace.dtl b/apps/dreki_web/templates/namespace.dtl
new file mode 100644
index 0000000..4f6e46d
--- /dev/null
+++ b/apps/dreki_web/templates/namespace.dtl
@@ -0,0 +1,6 @@
+<h1 class="text-lg">{{ namespace }}</h1>
+
+<ul>
+{% for store in stores %}
+<li><a href="{{ store.href }}">{{ store.name }}</a> ({{ store.backend_mod }})</a></li>
+{% endfor %}
diff --git a/apps/dreki_web/templates/store_list.dtl b/apps/dreki_web/templates/store_list.dtl
new file mode 100644
index 0000000..65dc5af
--- /dev/null
+++ b/apps/dreki_web/templates/store_list.dtl
@@ -0,0 +1,9 @@
+<h1 class="text-lg">{{ location }} :: {{ namespace }}:{{ directory }}</h1>
+
+<ul>
+{% for res in result %}
+<li><a href="{{ res.href }}">{{ res.id }}</a></li>
+{% endfor %}
+</ul>
+
+<a href="{{ new }}">Create</a>
diff --git a/apps/dreki_web/templates/store_new.dtl b/apps/dreki_web/templates/store_new.dtl
new file mode 100644
index 0000000..5a85854
--- /dev/null
+++ b/apps/dreki_web/templates/store_new.dtl
@@ -0,0 +1,18 @@
+<h1 class="text-lg">
+ <div class="text-base">
+ {{ location }} / {{ namespace }} / {{ directory }}
+ </div>
+ New
+</h1>
+
+<form action="{{ target }}" method="{{ method }}">
+ {{ form | safe }}
+
+ <div class="pt-5">
+ <div class="flex justify-end">
+ <button type="submit" class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">Save</button>
+ </div>
+ </div>
+
+</form>
+
diff --git a/apps/dreki_web/templates/store_show.dtl b/apps/dreki_web/templates/store_show.dtl
new file mode 100644
index 0000000..e944bee
--- /dev/null
+++ b/apps/dreki_web/templates/store_show.dtl
@@ -0,0 +1,8 @@
+<h1 class="text-lg">
+ <div class="text-base">
+ {{ location }} / {{ namespace }} / {{ directory }}
+ </div>
+ {{ result.id }}
+</h1>
+
+
diff --git a/apps/dreki_web/templates/tags/loading b/apps/dreki_web/templates/tags/loading
new file mode 100644
index 0000000..415bde0
--- /dev/null
+++ b/apps/dreki_web/templates/tags/loading
@@ -0,0 +1,7 @@
+<div class="flex{% if class %} {{ class }}{% end %}">
+ <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-primary-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+ </svg>
+ {% if title %}{{ title }}{% else %}Loading ...{% end %}
+</div>
diff --git a/apps/dreki_web/templates/task.dtl b/apps/dreki_web/templates/task.dtl
new file mode 100644
index 0000000..de9afaf
--- /dev/null
+++ b/apps/dreki_web/templates/task.dtl
@@ -0,0 +1,106 @@
+<div class="lg:flex lg:items-center lg:justify-between">
+ <div class="flex-1 min-w-0">
+ <nav class="flex" aria-label="Breadcrumb">
+ <ol role="list" class="flex items-center space-x-4">
+ <li>
+ <div class="flex">
+ <a href="/admin/tasks" class="text-sm font-medium text-gray-500 hover:text-gray-700">Tasks</a>
+ </div>
+ </li>
+ <!--<li>
+ <div class="flex items-center">
+ <svg class="flex-shrink-0 h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
+ </svg>
+ <a href="/admin/tasks" class="ml-4 text-sm font-medium text-gray-500 hover:text-gray-700">Tasks</a>
+ </div>
+ </li>-->
+ </ol>
+ </nav>
+
+ <h2 class="mt-2 text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">{{task.id}}</h2>
+
+ <!-- Sub info -->
+ <div class="mt-1 flex flex-col sm:flex-row sm:flex-wrap sm:mt-0 sm:space-x-6">
+ <div class="mt-2 flex items-center text-medium text-gray-700">
+ {{task.description}}
+ </div>
+ <div class="mt-2 flex items-center text-sm text-gray-500">
+ <!-- Heroicon name: solid/briefcase -->
+ <svg class="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M6 6V5a3 3 0 013-3h2a3 3 0 013 3v1h2a2 2 0 012 2v3.57A22.952 22.952 0 0110 13a22.95 22.95 0 01-8-1.43V8a2 2 0 012-2h2zm2-1a1 1 0 011-1h2a1 1 0 011 1v1H8V5zm1 5a1 1 0 011-1h.01a1 1 0 110 2H10a1 1 0 01-1-1z" clip-rule="evenodd" />
+ <path d="M2 13.692V16a2 2 0 002 2h12a2 2 0 002-2v-2.308A24.974 24.974 0 0110 15c-2.796 0-5.487-.46-8-1.308z" />
+ </svg>
+ {{task.handler}}
+ </div>
+ </div>
+
+ </div>
+
+ <!-- Actions -->
+ <div class="mt-5 flex lg:mt-0 lg:ml-4">
+ <span class="hidden sm:block">
+ <button type="button" class="inline-flex items-center px-4 py-2 border border-primary-300 rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
+ Execute
+ </button>
+ </span>
+
+ <span class="hidden sm:block ml-3">
+ <button type="button" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
+ Edit
+ </button>
+ </span>
+ </div>
+
+</div>
+
+<div class="px-4 sm:px-6 lg:px-8 sm:mt-8 lg:mt-12">
+ <div class="sm:flex sm:items-center">
+ <div class="sm:flex-auto">
+ <h1 class="text-xl font-semibold text-gray-900">Manifest</h1>
+ </div>
+ </div>
+ <pre class="px-2 py-2 bg-gray-50"><code>{{ task_pretty_params }}</code></pre>
+</div>
+
+
+<div class="px-4 sm:px-6 lg:px-8 sm:mt-8 lg:mt-12">
+ <div class="sm:flex sm:items-center">
+ <div class="sm:flex-auto">
+ <h1 class="text-xl font-semibold text-gray-900">Executions</h1>
+ </div>
+ </div>
+ <div class="mt-8 flex flex-col">
+ <div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
+ <div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
+ <div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
+ <table class="min-w-full divide-y divide-gray-300 table-auto">
+ <thead class="bg-gray-50">
+ <tr>
+ <th scope="col" class="whitespace-nowrap py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">ID</th>
+ <th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900">Description</th>
+ <th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900">Handler</th>
+ <th scope="col" class="relative whitespace-nowrap py-3.5 pl-3 pr-4 sm:pr-6">
+ <span class="sr-only">Edit</span>
+ </th>
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-gray-200 bg-white">
+ {% for t in tasks %}
+ <tr>
+ <td class="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-gray-500 sm:pl-6"><a href="/admin/tasks/{{t.id}}">{{t.id}}</a></td>
+ <td class="whitespace-nowrap px-2 py-2 text-sm font-medium text-gray-900">{{t.description}}</td>
+ <td class="whitespace-nowrap px-2 py-2 text-sm text-gray-900">{{t.handler}}</td>
+ <td class="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
+ <a href="#" class="text-primary-600 hover:text-primary-900">Edit<span class="sr-only">, {{t.id}}</span></a>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
diff --git a/apps/dreki_web/templates/tasks.dtl b/apps/dreki_web/templates/tasks.dtl
new file mode 100644
index 0000000..406094a
--- /dev/null
+++ b/apps/dreki_web/templates/tasks.dtl
@@ -0,0 +1,51 @@
+{% for source in sources %}
+<turbo-frame id="src-{{ source.path }}" src="{{ source.url }}">
+Loading {{ source.url }}
+</turbo-frame>
+{% endfor %}
+<div class="px-4 sm:px-6 lg:px-8">
+ <div class="sm:flex sm:items-center">
+ <div class="sm:flex-auto">
+ <h1 class="text-xl font-semibold text-gray-900">Tasks</h1>
+ <p class="mt-2 text-sm text-gray-700">
+ A table of placeholder stock market data that does not make any sense.
+ </p>
+ </div>
+ <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
+ <button type="button" class="inline-flex items-center justify-center rounded-md border border-transparent bg-primary-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 sm:w-auto">New</button>
+ </div>
+ </div>
+ <div class="mt-8 flex flex-col">
+ <div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
+ <div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
+ <div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
+ <table class="min-w-full divide-y divide-gray-300 table-auto">
+ <thead class="bg-gray-50">
+ <tr>
+ <th scope="col" class="whitespace-nowrap py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">ID</th>
+ <th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900">Description</th>
+ <th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900">Handler</th>
+ <th scope="col" class="relative whitespace-nowrap py-3.5 pl-3 pr-4 sm:pr-6">
+ <span class="sr-only">Edit</span>
+ </th>
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-gray-200 bg-white">
+ {% for t in tasks %}
+ <tr>
+ <td class="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-gray-500 sm:pl-6"><a href="/admin/tasks/{{t.id}}">{{t.id}}</a></td>
+ <td class="whitespace-nowrap px-2 py-2 text-sm font-medium text-gray-900">{{t.description}}</td>
+ <td class="whitespace-nowrap px-2 py-2 text-sm text-gray-900">{{t.handler}}</td>
+ <td class="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
+ <a href="#" class="text-primary-600 hover:text-primary-900">Edit<span class="sr-only">, {{t.id}}</span></a>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
diff --git a/apps/dreki_web/templates/world_graph_dot.dtl b/apps/dreki_web/templates/world_graph_dot.dtl
new file mode 100644
index 0000000..4a0fde0
--- /dev/null
+++ b/apps/dreki_web/templates/world_graph_dot.dtl
@@ -0,0 +1,11 @@
+graph {
+ class="world-graph dark:bg-gray-800 bg-gray-200 text-white dark:text-gray-200"
+ bgcolor="transparent"
+{% for vertice in vertices %}
+ "{{vertice.node}}" [label="{{vertice.name}}"] [class="world-graph-vertice-{{ vertice.type }}"] [color=white] [style=filled] [fillcolor="{{vertice.color}}"] [shape={{vertice.shape}}]
+{% endfor %}
+
+{% for edge in edges %}
+ "{{edge.from}}" -- "{{edge.to}}"
+{% endfor %}
+}
diff --git a/config/sys.config b/config/sys.config
new file mode 100644
index 0000000..be53a1f
--- /dev/null
+++ b/config/sys.config
@@ -0,0 +1,107 @@
+[
+{kernel, [
+ {logger_level, info},
+ {logger, [
+ {handler, default, logger_std_h, #{
+ formatter =>
+ {logger_colorful_formatter, #{
+ colors => #{debug => blue, notice => {bright, green}, warning => yellow, error => red,
+ critical => {bright, red}, alert => {bright, magenta}, emergency => {bg, red}},
+ template => [time," [",level,"] ", file,":",line," ",msg,"\n"]
+ }}
+ }}
+%%#{ formatter =>
+%% {logger_colorful_formatter, #{}} %%#{ template => [time," [",level,"] ", file,":",line," ",msg,"\n\n"] }}
+%% }}
+ ]}
+]},
+
+%%{lager, [
+%% {error_logger_redirect, false},
+%% {error_logger_whitelist, [logger_std_h, logger_colorful_formatter]}
+%%]},
+
+{opentelemetry, [
+ {span_processor, batch},
+ {exporter, {otel_exporter_stdout, []}},
+ {text_map_propagators, [baggage, trace_context]}
+]},
+
+{opentelemetry_exporter, [
+ {oltp_protocol, grpc},
+ {otlp_endpoint, "https://tempo-us-central1.grafana.net:443"},
+ {oltp_headers, [
+ %% 38972:eyJrIjoiMzg1ZjEwZTg3YjU0ZDY4ZGQzZTg3MzllNzU4NGZlZjI1NmQ5YWRhMCIsIm4iOiJkcmVraSBkZXYiLCJpZCI6NDg1NjI5fQ==
+ {"authorization",
+ "Basic Mzg5NzI6ZXlKcklqb2lNemcxWmpFd1pUZzNZalUwWkRZNFpHUXpaVGczTXpsbE56VTROR1psWmpJMU5tUTVZV1JoTUNJc0ltNGlPaUprY21WcmFTQmtaWFlpTENKcFpDSTZORGcxTmpJNWZRPT0="}
+ ]}
+]},
+
+{plum_db, [
+ {aae_enabled, true},
+ {store_open_retries_delay, 2000},
+ {store_open_retry_limit, 30},
+ {data_exchange_timeout, 60000},
+ {hashtree_timer, 10000},
+ {data_dir, "data/plumdb"},
+ {partitions, 8},
+ {prefixes, [
+ {state, ram},
+ {world, ram_disk},
+ {names, ram_disk},
+ {regions, ram_disk},
+ {nodes, ram_disk},
+ {paths, ram_disk},
+ {tasks, ram_disk},
+ {objects, ram_disk},
+ {stores, ram_disk},
+ {'idx:roles', ram_disk}, {'idx:tags', ram_disk}
+ ]}
+]},
+{partisan, [
+ {peer_ip, {0,0,0,0}},
+ {peer_port, 18086}, % port for inter-node communication
+ {parallelism, 4}, % number of tcp connections
+ {pid_encoding, false},
+ {ref_encoding, false},
+ {exchange_tick_period, 60000},
+ {lazy_tick_period, 1000},
+ {partisan_peer_service_manager,
+ partisan_pluggable_peer_service_manager}
+]},
+{plumtree, [
+ {broadcast_exchange_timer, 60000} % Perform AAE exchange every 1 min.
+]},
+ {dreki, [
+ {root_domain, <<"random.sh">>},
+ {internal_domain, <<"inf.random.sh">>},
+ {domain, <<"mgmt.stairway.dc2.scw.fr.eu.inf.random.sh">>},
+ {local_tasks_stores, [
+ {<<"local">>, dreki_dets_tasks, #{}, #{}}
+ ]},
+ {local_names_store, [
+ %% Store for *.DOMAIN
+ {local, dreki_dets_names, []},
+ %% Store for XXX.DOMAIN
+ {<<"service">>, dreki_dets_names, []}
+ ]},
+ {local_sequences_store, {dreki_dets_sequences, []}}
+ ]},
+ {ory, [
+ {kratos_url, <<"https://kratos.sso.internal.random.sh">>},
+ {hydra_url, <<"https://hydra.sso.internal.random.sh">>},
+ {keto_url, <<"https://keto.sso.internal.random.sh">>},
+ {hackney_ssl_opts, [
+ {verify, verify_peer},
+ {versions, ['tlsv1.2', 'tlsv1.3']},
+ {cacertfile, "/usr/local/etc/ssl/certs/ca.internal.random.sh.crt"},
+ {crl_check, false},
+ {crl_cache, {ssl_crl_cache, {internal, [{http, 5000}]}}}
+ ]}
+ ]},
+ {dreki_web, [
+ {transport, [
+ {port, 5000}
+ ]}
+ ]}
+].
diff --git a/config/vm.args b/config/vm.args
new file mode 100644
index 0000000..7ac00c1
--- /dev/null
+++ b/config/vm.args
@@ -0,0 +1,17 @@
+-name dreki@mgmt.stairway.dc2.scw.fr.eu.inf.random.sh
+
+-setcookie dreki_cookie
+
++K true
+
+## async
++A 30
+
+## Enable Time Correction
++c true
+
+## Enable multi_time_warp
++C multi_time_warp
+
+## Max processes
++P 2000000
diff --git a/rebar.config b/rebar.config
new file mode 100644
index 0000000..05d8e4c
--- /dev/null
+++ b/rebar.config
@@ -0,0 +1,84 @@
+{erl_opts, [debug_info]}.
+
+{deps, [
+ logger_colorful,
+ {opentelemetry_api, "~> 1.0"},
+ {opentelemetry, "~> 1.0"},
+ {opentelemetry_exporter, "1.0.2"},
+ {uuid, "2.0.4", {pkg, uuid_erl}},
+ {ory, {git, "https://git.random.sh/erlang-ory.git", {branch, "main"}}},
+ {mnesia_rocksdb, {git, "https://github.com/aeternity/mnesia_rocksdb", {branch, "master"}}},
+ {ra, "2.0.6"},
+ {lager, {git, "https://github.com/erlang-lager/lager", {branch, "master"}}},
+ {partisan, {git, "http://github.com/aramallo/partisan.git", {branch, "master"}}},
+ {plum_db, {git, "https://gitlab.com/leapsight/plum_db", {branch, "master"}}},
+ {khepri, {git, "https://github.com/rabbitmq/khepri", {branch, "main"}}},
+ {dns_erlang, {git, "https://github.com/dnsimple/dns_erlang", {branch, "main"}}},
+ {erldns, {git, "https://github.com/dnsimple/erldns", {branch, "main"}}},
+ {yamerl, "0.10.0"},
+ {jsone, "1.6.1"},
+ {jsx, "3.1.0"},
+ {jesse, {git, "https://github.com/for-GET/jesse", {branch, "master"}}},
+ {fast_yaml, {git, "https://github.com/processone/fast_yaml", {branch, "master"}}},
+%% {jsonnet, {git, "https://github.com/ray2501/erlang-jsonnet", {branch, "master"}}},
+ datalog
+]}.
+
+{plugins, [
+ rebar3_run,
+ rebar3_depup,
+ rebar3_lint
+]}.
+
+%%{elvis_output_format, plain | colors | parsable}.
+{elvis_output_format, parsable}.
+{elvis, [
+ #{ dirs => ["apps/*/src/**", "src/**"],
+ filter => "*.erl",
+ ruleset => erl_files },
+ #{ dirs => ["."],
+ filter => "rebar.config",
+ ruleset => rebar_config }
+ %%#{ dirs => ["."],
+ %% filter => "elvis.config",
+ %% ruleset => elvis_config }
+]}.
+
+{relx, [{release, {dreki, "0.1.0"},
+ [opentelemetry_exporter, {opentelemetry, temporary},
+ dreki,
+ sasl]},
+ {mode, dev},
+ {sys_config, "./config/sys.config"},
+ {vm_args, "./config/vm.args"},
+ %% the .src form of the configuration files do
+ %% not require setting RELX_REPLACE_OS_VARS
+ %% {sys_config_src, "./config/sys.config.src"},
+ %% {vm_args_src, "./config/vm.args.src"}
+ {extended_start_script, true}
+]}.
+
+{project_app_dirs, ["apps/*"]}.
+
+{profiles, [
+ {prod, [{relx,
+ [%% prod is the default mode when prod
+ %% profile is used, so does not have
+ %% to be explicitly included like this
+ {mode, prod}
+
+ %% use minimal mode to exclude ERTS
+ %% {mode, minimal}
+ ]}
+ ]},
+
+ {mgmt2, [
+ {relx, [
+ {sys_config, "./config/mgmt2/sys.config"},
+ {vm_args, "./config/mgmt2/vm.args"},
+ {overlay, [
+ {mkdir, "./data"}
+ ]}
+ ]}
+ ]}
+]}.
diff --git a/rebar.lock b/rebar.lock
new file mode 100644
index 0000000..2f0a5bf
--- /dev/null
+++ b/rebar.lock
@@ -0,0 +1,256 @@
+{"1.2.0",
+[{<<"acceptor_pool">>,{pkg,<<"acceptor_pool">>,<<"1.0.0">>},1},
+ {<<"app_config">>,
+ {git,"https://gitlab.com/leapsight/app_config.git",
+ {ref,"e6a4dc99c0c9f17a6d4d865e40aefc409986f849"}},
+ 1},
+ {<<"aten">>,{pkg,<<"aten">>,<<"0.5.7">>},1},
+ {<<"base32">>,{pkg,<<"base32">>,<<"0.1.0">>},1},
+ {<<"base62">>,
+ {git,"https://gitlab.com/leapsight/base62.git",
+ {ref,"6e1c622c2e9bba51fadba44fa050d4b4ae706890"}},
+ 3},
+ {<<"base64url">>,{pkg,<<"base64url">>,<<"0.0.1">>},2},
+ {<<"bear">>,{pkg,<<"bear">>,<<"0.8.7">>},2},
+ {<<"certifi">>,{pkg,<<"certifi">>,<<"2.6.1">>},2},
+ {<<"chatterbox">>,{pkg,<<"ts_chatterbox">>,<<"0.11.0">>},2},
+ {<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.9.0">>},0},
+ {<<"cowboy_telemetry">>,{pkg,<<"cowboy_telemetry">>,<<"0.4.0">>},0},
+ {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.11.0">>},1},
+ {<<"ctx">>,{pkg,<<"ctx">>,<<"0.6.0">>},2},
+ {<<"datalog">>,{pkg,<<"datalog">>,<<"2.0.2">>},0},
+ {<<"datum">>,{pkg,<<"datum">>,<<"4.5.1">>},1},
+ {<<"dns_erlang">>,
+ {git,"https://github.com/dnsimple/dns_erlang",
+ {ref,"52f7279afb4cc97b4d76babde223257ec08f3676"}},
+ 0},
+ {<<"eleveldb">>,
+ {git,"https://github.com/Leapsight/eleveldb.git",
+ {ref,"f7339ad2a57537b3f7e185a38341664d3cc22cfb"}},
+ 1},
+ {<<"erldns">>,
+ {git,"https://github.com/dnsimple/erldns",
+ {ref,"3ddea6d2a6e1d3dc4190294db9d2d50a182425c2"}},
+ 0},
+ {<<"erlexec">>,
+ {git,"https://github.com/saleyn/erlexec",
+ {ref,"dd6fb59a82ad0b920baee97271826b1ae255d1b4"}},
+ 0},
+ {<<"erlsom">>,{pkg,<<"erlsom">>,<<"1.5.0">>},2},
+ {<<"erlydtl">>,{pkg,<<"erlydtl">>,<<"0.14.0">>},0},
+ {<<"fast_yaml">>,
+ {git,"https://github.com/processone/fast_yaml",
+ {ref,"b359c1fde1e16b66dbc0abca596af7e9e2179cd7"}},
+ 0},
+ {<<"folsom">>,{pkg,<<"folsom">>,<<"0.8.8">>},1},
+ {<<"gen_batch_server">>,{pkg,<<"gen_batch_server">>,<<"0.8.7">>},1},
+ {<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},1},
+ {<<"gproc">>,{pkg,<<"gproc">>,<<"0.9.0">>},1},
+ {<<"grpcbox">>,{pkg,<<"grpcbox">>,<<"0.14.0">>},1},
+ {<<"hackney">>,{pkg,<<"hackney">>,<<"1.17.4">>},1},
+ {<<"hpack">>,{pkg,<<"hpack_erl">>,<<"0.2.3">>},3},
+ {<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},2},
+ {<<"iso8601">>,{pkg,<<"iso8601">>,<<"1.3.1">>},1},
+ {<<"jesse">>,
+ {git,"https://github.com/for-GET/jesse",
+ {ref,"eb35eff3a78b4cc41dd890562f4f93cdac552b6d"}},
+ 0},
+ {<<"jose">>,
+ {git,"https://github.com/potatosalad/erlang-jose.git",
+ {ref,"090a2ed054304ecc012d6c2d9d10d2a294d835b1"}},
+ 1},
+ {<<"jsone">>,{pkg,<<"jsone">>,<<"1.6.1">>},0},
+ {<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},0},
+ {<<"key_value">>,
+ {git,"https://gitlab.com/leapsight/key_value.git",
+ {ref,"414fb19cd067b368666ceb5a2a63f1f24109562b"}},
+ 2},
+ {<<"khepri">>,
+ {git,"https://github.com/rabbitmq/khepri",
+ {ref,"53a8ad8022369b07ccaf34693b0d6b538b51f810"}},
+ 0},
+ {<<"ksuid">>,
+ {git,"https://gitlab.com/leapsight/ksuid.git",
+ {ref,"a5016017edd1e99d46c9a6d4c348cb818e542b13"}},
+ 2},
+ {<<"lager">>,
+ {git,"https://github.com/erlang-lager/lager",
+ {ref,"a140ea935eae9149bb35234bb40f6acf1c69caa1"}},
+ 0},
+ {<<"logger_colorful">>,{pkg,<<"logger_colorful">>,<<"0.1.0">>},0},
+ {<<"meck">>,{pkg,<<"meck">>,<<"0.9.2">>},1},
+ {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2},
+ {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},2},
+ {<<"mnesia_rocksdb">>,
+ {git,"https://github.com/aeternity/mnesia_rocksdb",
+ {ref,"c0ce3afe394c02aec0ab18c0b9d5b61ab6610c7f"}},
+ 0},
+ {<<"nodefinder">>,{pkg,<<"nodefinder">>,<<"2.0.0">>},1},
+ {<<"oauth2c">>,
+ {git,"https://github.com/kivra/oauth2_client",
+ {ref,"47ba50a57e981253246b732cf31fb4df83fa260f"}},
+ 0},
+ {<<"observer_cli">>,{pkg,<<"observer_cli">>,<<"1.7.1">>},1},
+ {<<"opentelemetry">>,{pkg,<<"opentelemetry">>,<<"1.0.2">>},0},
+ {<<"opentelemetry_api">>,{pkg,<<"opentelemetry_api">>,<<"1.0.2">>},0},
+ {<<"opentelemetry_cowboy">>,{pkg,<<"opentelemetry_cowboy">>,<<"0.1.0">>},0},
+ {<<"opentelemetry_exporter">>,
+ {pkg,<<"opentelemetry_exporter">>,<<"1.0.2">>},
+ 0},
+ {<<"opentelemetry_telemetry">>,
+ {pkg,<<"opentelemetry_telemetry">>,<<"1.0.0-beta.7">>},
+ 1},
+ {<<"ory">>,
+ {git,"https://git.random.sh/erlang-ory.git",
+ {ref,"ec73e94c592e069f6abd44b292d156184a82c3ed"}},
+ 0},
+ {<<"p1_utils">>,{pkg,<<"p1_utils">>,<<"1.0.23">>},1},
+ {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.1">>},2},
+ {<<"partisan">>,
+ {git,"http://github.com/aramallo/partisan.git",
+ {ref,"22c6072979c7f814e0884417e0980f11bdaa390c"}},
+ 0},
+ {<<"plum_db">>,
+ {git,"https://gitlab.com/leapsight/plum_db",
+ {ref,"49ab98faed623e26a2293eac0b73bed34b02e47a"}},
+ 0},
+ {<<"quickrand">>,{pkg,<<"quickrand">>,<<"2.0.4">>},1},
+ {<<"ra">>,{pkg,<<"ra">>,<<"2.0.6">>},0},
+ {<<"ranch">>,{pkg,<<"ranch">>,<<"1.8.0">>},1},
+ {<<"recon">>,{pkg,<<"recon">>,<<"2.5.2">>},1},
+ {<<"restc">>,
+ {git,"https://github.com/kivra/restclient.git",
+ {ref,"993fdb6005da69252ec98573b2c4ea5a905011ff"}},
+ 1},
+ {<<"rfc3339">>,{pkg,<<"rfc3339">>,<<"0.2.2">>},1},
+ {<<"rocksdb">>,{pkg,<<"rocksdb">>,<<"1.7.0">>},1},
+ {<<"sext">>,{pkg,<<"sext">>,<<"1.8.0">>},1},
+ {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.6">>},2},
+ {<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.1.0">>},1},
+ {<<"telemetry_registry">>,{pkg,<<"telemetry_registry">>,<<"0.3.0">>},2},
+ {<<"tls_certificate_check">>,
+ {pkg,<<"tls_certificate_check">>,<<"1.13.0">>},
+ 1},
+ {<<"trails">>,{pkg,<<"trails">>,<<"2.3.0">>},0},
+ {<<"types">>,{pkg,<<"types">>,<<"0.1.8">>},1},
+ {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},2},
+ {<<"utils">>,
+ {git,"https://gitlab.com/leapsight/utils.git",
+ {ref,"881e59760358bf22979d6ef1fcd96fc476c8b3ca"}},
+ 1},
+ {<<"uuid">>,{pkg,<<"uuid_erl">>,<<"2.0.4">>},0},
+ {<<"yamerl">>,{pkg,<<"yamerl">>,<<"0.10.0">>},0}]}.
+[
+{pkg_hash,[
+ {<<"acceptor_pool">>, <<"43C20D2ACAE35F0C2BCD64F9D2BDE267E459F0F3FD23DAB26485BF518C281B21">>},
+ {<<"aten">>, <<"F88EB38CEADC0710B84B5D78F0357D766733D80902D50AC69B70CDF834992DAA">>},
+ {<<"base32">>, <<"044F6DC95709727CA2176F3E97A41DDAA76B5BC690D3536908618C0CB32616A2">>},
+ {<<"base64url">>, <<"36A90125F5948E3AFD7BE97662A1504B934DD5DAC78451CA6E9ABF85A10286BE">>},
+ {<<"bear">>, <<"16264309AE5D005D03718A5C82641FCC259C9E8F09ADEB6FD79CA4271168656F">>},
+ {<<"certifi">>, <<"DBAB8E5E155A0763EEA978C913CA280A6B544BFA115633FA20249C3D396D9493">>},
+ {<<"chatterbox">>, <<"B8F372C706023EB0DE5BF2976764EDB27C70FE67052C88C1F6A66B3A5626847F">>},
+ {<<"cowboy">>, <<"865DD8B6607E14CF03282E10E934023A1BD8BE6F6BACF921A7E2A96D800CD452">>},
+ {<<"cowboy_telemetry">>, <<"F239F68B588EFA7707ABCE16A84D0D2ACF3A0F50571F8BB7F56A15865AAE820C">>},
+ {<<"cowlib">>, <<"0B9FF9C346629256C42EBE1EEB769A83C6CB771A6EE5960BD110AB0B9B872063">>},
+ {<<"ctx">>, <<"8FF88B70E6400C4DF90142E7F130625B82086077A45364A78D208ED3ED53C7FE">>},
+ {<<"datalog">>, <<"E51D71086AAA8470E79224225905E814C764A46800378E6DB00AC2F888651F82">>},
+ {<<"datum">>, <<"66C163A7FFB7A773E884EB238402375D2E6609718C5C88AEF5809E410D494F08">>},
+ {<<"erlsom">>, <<"C5A5CDD0EE0E8DCA62BCC4B13FF08DA24FDEFC16CCD8B25282A2FDA2BA1BE24A">>},
+ {<<"erlydtl">>, <<"964B2DC84F8C17ACFAA69C59BA129EF26AC45D2BA898C3C6AD9B5BDC8BA13CED">>},
+ {<<"folsom">>, <<"9A2B02010F6727CB1948EF34E21CB66F3554B63355C3A31E8BCB10FB172C3170">>},
+ {<<"gen_batch_server">>, <<"1D91A3605D3110EC791F00D5968E86E62FD4C6979E760796A51D50A7ACC7A40D">>},
+ {<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>},
+ {<<"gproc">>, <<"853CCB7805E9ADA25D227A157BA966F7B34508F386A3E7E21992B1B484230699">>},
+ {<<"grpcbox">>, <<"3EB321BCD2275BAF8B54CF381FEB7B0559A50C02544DE28FDA039C7F2F9D1A7A">>},
+ {<<"hackney">>, <<"99DA4674592504D3FB0CFEF0DB84C3BA02B4508BAE2DFF8C0108BAA0D6E0977C">>},
+ {<<"hpack">>, <<"17670F83FF984AE6CD74B1C456EDDE906D27FF013740EE4D9EFAA4F1BF999633">>},
+ {<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>},
+ {<<"iso8601">>, <<"D1CEE73F56D71C35590C6B2DB2074873BF410BABAAB768F6EA566366D8CA4810">>},
+ {<<"jsone">>, <<"7EA1098FE004C4127320FE0E3CF6A951B01F82039FEAA56C322DC7E34DD59762">>},
+ {<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>},
+ {<<"logger_colorful">>, <<"548A7F21C7F2F6713CD2E1A7B1323B2AD712E11AE5913D2FB05BE6F34B799081">>},
+ {<<"meck">>, <<"85CCBAB053F1DB86C7CA240E9FC718170EE5BDA03810A6292B5306BF31BAE5F5">>},
+ {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>},
+ {<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>},
+ {<<"nodefinder">>, <<"1FA140D4C3A27FF66623E267E66876AF18817BA82AD303E4AE46ADBC941851AA">>},
+ {<<"observer_cli">>, <<"C9CA1F623A3EF0158283A3C37CD7B7235BFE85927AD6E26396DD247E2057F5A1">>},
+ {<<"opentelemetry">>, <<"9FFA9DDCBEC9356154681BC9D0A54BB20F0DE0E8C6696CCC298B49633308782B">>},
+ {<<"opentelemetry_api">>, <<"91353EE40583B1D4F07D7B13ED62642ABFEC6AAA0D8A2114F07EDAFB2DF781C5">>},
+ {<<"opentelemetry_cowboy">>, <<"CE16BE94932DFCF9A036DE0B516413A5180AA6A4F31AD4073DD86EABE73F8837">>},
+ {<<"opentelemetry_exporter">>, <<"19A102D1F04776399A915BE27121852468318C27146E553FAF28008E3E474972">>},
+ {<<"opentelemetry_telemetry">>, <<"BA1DF62515AED63F99A80DDF17E7A3873D1F686F23598EDEBF1633942772856E">>},
+ {<<"p1_utils">>, <<"7F94466ADA69BD982EA7BB80FBCA18E7053E7D0B82C9D9E37621FA508587069B">>},
+ {<<"parse_trans">>, <<"16328AB840CC09919BD10DAB29E431DA3AF9E9E7E7E6F0089DD5A2D2820011D8">>},
+ {<<"quickrand">>, <<"168CA3A8466A26912B8C3A1D6AA58975E1BB49E5C7AFB4998B80F6B90F910490">>},
+ {<<"ra">>, <<"C1AD68DE00B5DD3B32E6A30E1359AC676C3CAE47EECBA951789B470CBD4E1087">>},
+ {<<"ranch">>, <<"8C7A100A139FD57F17327B6413E4167AC559FBC04CA7448E9BE9057311597A1D">>},
+ {<<"recon">>, <<"CBA53FA8DB83AD968C9A652E09C3ED7DDCC4DA434F27C3EAA9CA47FFB2B1FF03">>},
+ {<<"rfc3339">>, <<"1552DF616ACA368D982E9F085A0E933B6688A3F4938A671798978EC2C0C58730">>},
+ {<<"rocksdb">>, <<"5D23319998A7FCE5FFD5D7824116C905CABA7F91BAF8EDDABD0180F1BB272CEF">>},
+ {<<"sext">>, <<"90A95B889F5C781B70BBCF44278B763148E313C376B60D87CE664CB1C1DD29B5">>},
+ {<<"ssl_verify_fun">>, <<"CF344F5692C82D2CD7554F5EC8FD961548D4FD09E7D22F5B62482E5AEAEBD4B0">>},
+ {<<"telemetry">>, <<"A589817034A27EAB11144AD24D5C0F9FAB1F58173274B1E9BAE7074AF9CBEE51">>},
+ {<<"telemetry_registry">>, <<"6768F151EA53FC0FBCA70DBFF5B20A8D663EE4E0C0B2AE589590E08658E76F1E">>},
+ {<<"tls_certificate_check">>, <<"C407200CA837DF4E6CF533874E9C4C8BCDE2E71520AB215856BDAEC9C7FB9252">>},
+ {<<"trails">>, <<"B09703F056705F4943E14FFF077B98C711A6F48FAD40F4FF0B350794074AD69C">>},
+ {<<"types">>, <<"5782B67231E8C174FE2835395E71E669FE0121076779D2A09F1C0D58EE0E2F13">>},
+ {<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>},
+ {<<"uuid">>, <<"77C3E3EE1E1701A2856CE945846D7CEB71931C60633A305D0B0FEAE03B2B3B5C">>},
+ {<<"yamerl">>, <<"4FF81FEE2F1F6A46F1700C0D880B24D193DDB74BD14EF42CB0BCF46E81EF2F8E">>}]},
+{pkg_hash_ext,[
+ {<<"acceptor_pool">>, <<"0CBCD83FDC8B9AD2EEE2067EF8B91A14858A5883CB7CD800E6FCD5803E158788">>},
+ {<<"aten">>, <<"8B623C8BE27B59A911D16AB0AF41777B504C147BC0D60A29015FAB58321C04B0">>},
+ {<<"base32">>, <<"10A73951D857D8CB1ECEEA8EB96C6941F6A76E105947AD09C2B73977DEE07638">>},
+ {<<"base64url">>, <<"FAB09B20E3F5DB886725544CBCF875B8E73EC93363954EB8A1A9ED834AA8C1F9">>},
+ {<<"bear">>, <<"534217DCE6A719D59E54FB0EB7A367900DBFC5F85757E8C1F94269DF383F6D9B">>},
+ {<<"certifi">>, <<"524C97B4991B3849DD5C17A631223896272C6B0AF446778BA4675A1DFF53BB7E">>},
+ {<<"chatterbox">>, <<"722FE2BAD52913AB7E87D849FC6370375F0C961FFB2F0B5E6D647C9170C382A6">>},
+ {<<"cowboy">>, <<"2C729F934B4E1AA149AFF882F57C6372C15399A20D54F65C8D67BEF583021BDE">>},
+ {<<"cowboy_telemetry">>, <<"7D98BAC1EE4565D31B62D59F8823DFD8356A169E7FCBB83831B8A5397404C9DE">>},
+ {<<"cowlib">>, <<"2B3E9DA0B21C4565751A6D4901C20D1B4CC25CBB7FD50D91D2AB6DD287BC86A9">>},
+ {<<"ctx">>, <<"A14ED2D1B67723DBEBBE423B28D7615EB0BDCBA6FF28F2D1F1B0A7E1D4AA5FC2">>},
+ {<<"datalog">>, <<"2BE79EC55265AEDF69B98F7B205E23BC5F55BF4CF92D64093F7CF7CAFAC649AF">>},
+ {<<"datum">>, <<"20DCF2084B00143B8D9EEC45EEF4C8D436E77FDC8B23208CA7BB82B3BD6A4DA4">>},
+ {<<"erlsom">>, <<"55A9DBF9CFA77FCFC108BD8E2C4F9F784DEA228A8F4B06EA10B684944946955A">>},
+ {<<"erlydtl">>, <<"D80EC044CD8F58809C19D29AC5605BE09E955040911B644505E31E9DD8143431">>},
+ {<<"folsom">>, <<"22DACCB25764AC71C7C7C29964D7F9D4BB2C70858E0FD7816C2F630BACA8F5C9">>},
+ {<<"gen_batch_server">>, <<"94A49A528486298B009D2A1B452132C0A0D68B3E89D17D3764CB1EC879B7557A">>},
+ {<<"goldrush">>, <<"99CB4128CFFCB3227581E5D4D803D5413FA643F4EB96523F77D9E6937D994CEB">>},
+ {<<"gproc">>, <<"587E8AF698CCD3504CF4BA8D90F893EDE2B0F58CABB8A916E2BF9321DE3CF10B">>},
+ {<<"grpcbox">>, <<"E24159B7B6D3F9869BBE528845C0125FED2259366BA908FD04A1F45FE81D0660">>},
+ {<<"hackney">>, <<"DE16FF4996556C8548D512F4DBE22DD58A587BF3332E7FD362430A7EF3986B16">>},
+ {<<"hpack">>, <<"06F580167C4B8B8A6429040DF36CC93BBA6D571FAEAEC1B28816523379CBB23A">>},
+ {<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>},
+ {<<"iso8601">>, <<"A8B00594F4309A41D17BA4AEAB2B94DFE1F4BE99F263BC1F46DAC9002CE99A29">>},
+ {<<"jsone">>, <<"A6C1DF6081DF742068D2ED747A4FE8A7740C56421B53E02BC9D4907DD3502922">>},
+ {<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>},
+ {<<"logger_colorful">>, <<"D2A11586B2311FF2CC4B2C38835FE8C8768D2087AE9BF7D9EA80C8C33E92689D">>},
+ {<<"meck">>, <<"81344F561357DC40A8344AFA53767C32669153355B626EA9FCBC8DA6B3045826">>},
+ {<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>},
+ {<<"mimerl">>, <<"F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323">>},
+ {<<"nodefinder">>, <<"10A31C59A4249BFF181773AC77390A4416AA0EEE93C33C716A113419EBF3B210">>},
+ {<<"observer_cli">>, <<"4CCAFAAA2CE01B85DDD14591F4D5F6731B4E13B610A70FB841F0701178478280">>},
+ {<<"opentelemetry">>, <<"E774506333C8AFD9133FEE4BA69D06E6413CCAD6EE76F10E59C3EC624009AF23">>},
+ {<<"opentelemetry_api">>, <<"2A8247F85C44216B883900067478D59955D11E58E5CFCA7C884CD4F203ACE3AC">>},
+ {<<"opentelemetry_cowboy">>, <<"455263DD177ACB8724391A5F2AF809E99F5D4A6DC71684E34A462F4D4AD02BD0">>},
+ {<<"opentelemetry_exporter">>, <<"43DE904DD7F482009BF1A40D591AB5ED25F603F1072A04A162E87F7F8C08DBB5">>},
+ {<<"opentelemetry_telemetry">>, <<"480F4FA1E992D597F931E7BC9E68478E8D904AD84489D2C5CA6EB6D48BBD7801">>},
+ {<<"p1_utils">>, <<"47F21618694EEEE5006AF1C88731AD86B757161E7823C29B6F73921B571C8502">>},
+ {<<"parse_trans">>, <<"07CD9577885F56362D414E8C4C4E6BDF10D43A8767ABB92D24CBE8B24C54888B">>},
+ {<<"quickrand">>, <<"4CB18E9304CF28E054E8DC6E151D1AC7F174E6FE31D5C1A07F71279B92A90800">>},
+ {<<"ra">>, <<"75C664334D4F294327DD8AAD06385294CC5CCE4763FBEF13EBF5075E930CEDF6">>},
+ {<<"ranch">>, <<"49FBCFD3682FAB1F5D109351B61257676DA1A2FDBE295904176D5E521A2DDFE5">>},
+ {<<"recon">>, <<"2C7523C8DEE91DFF41F6B3D63CBA2BD49EB6D2FE5BF1EEC0DF7F87EB5E230E1C">>},
+ {<<"rfc3339">>, <<"986D7F9BAC6891AA4D5051690058DE4E623245620BBEADA7F239F85C4DF8F23C">>},
+ {<<"rocksdb">>, <<"A4BDC5DD80ED137161549713062131E8240523787EBE7B51DF61CFB48B1786CE">>},
+ {<<"sext">>, <<"BC6016CB8690BAF677EACACFE6E7CADFEC8DC7E286CBBED762F6CD55B0678E73">>},
+ {<<"ssl_verify_fun">>, <<"BDB0D2471F453C88FF3908E7686F86F9BE327D065CC1EC16FA4540197EA04680">>},
+ {<<"telemetry">>, <<"B727B2A1F75614774CFF2D7565B64D0DFA5BD52BA517F16543E6FC7EFCC0DF48">>},
+ {<<"telemetry_registry">>, <<"492E2ADBC609F3E79ECE7F29FEC363A97A2C484AC78A83098535D6564781E917">>},
+ {<<"tls_certificate_check">>, <<"B45A3117931B808FDAEA580F6E7ED1A7889C171DAE792E632A166D6DDA6E88A2">>},
+ {<<"trails">>, <<"40804001EB80417AA9D02400F39B7216956C3F251539A8A6096A69B3FAC0EA07">>},
+ {<<"types">>, <<"04285239F4954C5EDE56F78ED7778EDE24E3F2E997F7B16402A167AF0CC2658A">>},
+ {<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>},
+ {<<"uuid">>, <<"7A4CCD1C151D9B88B4383FA802BCCF9BCB3754B7F53D7CAA164D51A14A6652E4">>},
+ {<<"yamerl">>, <<"346ADB2963F1051DC837A2364E4ACF6EB7D80097C0F53CBDC3046EC8EC4B4E6E">>}]}
+].