-module(spectator). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/spectator.gleam"). -export([tag/2, tag_subject/2, tag_result/2, start_on/2, start/0, main/0]). -export_type([node_connection_error/0, socket/1]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. -type node_connection_error() :: not_distributed_error | failed_to_set_cookie_error | failed_to_connect_error. -type socket(ACGN) :: {socket, lustre:runtime(ACGN), gleam@erlang@process:subject(lustre@runtime@transport:client_message(ACGN))}. -file("src/spectator.gleam", 164). ?DOC( " Tag a process given by PID with a name for easier identification in the spectator UI.\n" " You must call `start` before calling this function.\n" ). -spec tag(gleam@erlang@process:pid_(), binary()) -> gleam@erlang@process:pid_(). tag(Pid, Name) -> spectator_tag_manager:add_tag(Pid, Name), Pid. -file("src/spectator.gleam", 171). ?DOC( " Tag a process given by subject with a name for easier identification in the spectator UI.\n" " You must call `start` before calling this function.\n" ). -spec tag_subject(gleam@erlang@process:subject(ACGU), binary()) -> gleam@erlang@process:subject(ACGU). tag_subject(Sub, Name) -> Pid@1 = case gleam@erlang@process:subject_owner(Sub) of {ok, Pid} -> Pid; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"spectator"/utf8>>, function => <<"tag_subject"/utf8>>, line => 175, value => _assert_fail, start => 5771, 'end' => 5818, pattern_start => 5782, pattern_end => 5789}) end, tag(Pid@1, Name), Sub. -file("src/spectator.gleam", 182). ?DOC( " Tag a process given by subject result with a name for easier identification in the spectator UI.\n" " You must call `start` before calling this function.\n" ). -spec tag_result( {ok, gleam@erlang@process:subject(ACGX)} | {error, ACGZ}, binary() ) -> {ok, gleam@erlang@process:subject(ACGX)} | {error, ACGZ}. tag_result(Result, Name) -> case Result of {ok, Sub} -> {ok, tag_subject(Sub, Name)}; Other -> Other end. -file("src/spectator.gleam", 198). -spec validate_node_connection(list({binary(), binary()})) -> {ok, binary()} | {error, node_connection_error()}. validate_node_connection(Params) -> Node_res = spectator@internal@common:get_param(Params, <<"node"/utf8>>), case Node_res of {error, _} -> Self = begin _pipe = erlang:node(), _pipe@1 = gleam_erlang_ffi:identity(_pipe), erlang:atom_to_binary(_pipe@1) end, {ok, Self}; {ok, Node} -> Self@1 = begin _pipe@2 = erlang:node(), gleam_erlang_ffi:identity(_pipe@2) end, gleam@bool:guard( Self@1 =:= erlang:binary_to_atom(<<"nonode@nohost"/utf8>>), {error, not_distributed_error}, fun() -> Node_atom = erlang:binary_to_atom(Node), Cookie_validation_passed = case spectator@internal@common:get_param( Params, <<"cookie"/utf8>> ) of {error, _} -> true; {ok, Cookie} -> Cookie_atom = erlang:binary_to_atom(Cookie), gleam@result:unwrap( spectator_ffi:set_cookie(Node_atom, Cookie_atom), false ) end, gleam@bool:guard( not Cookie_validation_passed, {error, failed_to_set_cookie_error}, fun() -> gleam@bool:guard( not gleam@result:unwrap( spectator_ffi:hidden_connect_node(Node_atom), false ), {error, failed_to_connect_error}, fun() -> {ok, erlang:atom_to_binary(Node_atom)} end ) end ) end ) end. -file("src/spectator.gleam", 241). -spec connect_widget_script() -> lustre@vdom@vnode:element(any()). connect_widget_script() -> lustre@element@html:script( [lustre@attribute:src(<<"/connect-widget.js"/utf8>>)], <<""/utf8>> ). -file("src/spectator.gleam", 245). -spec render_server_component(binary(), binary(), list({binary(), binary()})) -> gleam@http@response:response(mist:response_data()). render_server_component(Title, Path, Params) -> Res = gleam@http@response:new(200), Styles = spectator@internal@common:static_file(<<"styles.css"/utf8>>), Html = case validate_node_connection(Params) of {ok, Connection_name} -> lustre@element@html:html( [], [lustre@element@html:head( [], [lustre@element@html:title([], Title), lustre@server_component:script(), lustre@element@html:meta( [lustre@attribute:attribute( <<"charset"/utf8>>, <<"utf-8"/utf8>> )] ), lustre@element@html:link( [lustre@attribute:rel(<<"icon"/utf8>>), lustre@attribute:href( <<"/favicon.svg"/utf8>> ), lustre@attribute:type_( <<"image/svg+xml"/utf8>> )] ), lustre@element@html:style([], Styles)] ), lustre@element@html:body( [], [spectator@internal@views@navbar:render( Title, Connection_name, begin _pipe = spectator@internal@common:sanitize_params( Params ), spectator@internal@common:encode_params( _pipe ) end ), connect_widget_script(), lustre@element:element( <<"lustre-server-component"/utf8>>, [lustre@server_component:route( <<<<"/"/utf8, Path/binary>>/binary, (spectator@internal@common:encode_params( Params ))/binary>> )], [] )] )] ); {error, Connection_error} -> lustre@element@html:html( [], [lustre@element@html:head( [], [lustre@element@html:title( [], <> ), lustre@element@html:meta( [lustre@attribute:attribute( <<"charset"/utf8>>, <<"utf-8"/utf8>> )] ), lustre@element@html:link( [lustre@attribute:rel(<<"icon"/utf8>>), lustre@attribute:href( <<"/favicon.svg"/utf8>> ), lustre@attribute:type_( <<"image/svg+xml"/utf8>> )] ), lustre@element@html:style([], Styles)] ), lustre@element@html:body( [], [spectator@internal@views@navbar:render( Title, <<"Connection Failed"/utf8>>, begin _pipe@1 = spectator@internal@common:sanitize_params( Params ), spectator@internal@common:encode_params( _pipe@1 ) end ), lustre@element@html:'div'( [lustre@attribute:class( <<"component-error"/utf8>> )], [lustre@element@html:'div'( [], [lustre@element@html:text( <<"Node connection failed:"/utf8>> )] ), lustre@element@html:'div'( [], [lustre@element@html:text( case Connection_error of not_distributed_error -> <<"Node is not distributed, cannot connect to other nodes. Please start the spectator instance in distributed mode by setting a node name."/utf8>>; failed_to_set_cookie_error -> <<"Failed to set cookie, could not apply the cookie to the node"/utf8>>; failed_to_connect_error -> <<"Failed to connect to node, please check the node name and cookie"/utf8>> end )] ), lustre@element@html:'div'( [], [lustre@element@html:button( [lustre@attribute:class( <<"change-target-button"/utf8>> )], [lustre@element@html:text( <<"Change Inspection Target"/utf8>> )] )] )] ), connect_widget_script()] )] ) end, gleam@http@response:set_body( Res, begin _pipe@2 = Html, _pipe@3 = lustre@element:to_document_string(_pipe@2), _pipe@4 = gleam_stdlib:wrap_list(_pipe@3), {bytes, _pipe@4} end ). -file("src/spectator.gleam", 346). -spec connect_server_component( gleam@http@request:request(mist@internal@http:connection()), fun(() -> lustre:app(list({binary(), binary()}), any(), any())), list({binary(), binary()}) ) -> gleam@http@response:response(mist:response_data()). connect_server_component(Req, Lustre_application, Params) -> Socket_init = fun(_) -> Self = gleam@erlang@process:new_subject(), App = Lustre_application(), Component@1 = case lustre:start_server_component(App, Params) of {ok, Component} -> Component; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <<?FILEPATH/utf8>>, module => <<"spectator"/utf8>>, function => <<"connect_server_component"/utf8>>, line => 355, value => _assert_fail, start => 10863, 'end' => 10932, pattern_start => 10874, pattern_end => 10887}) end, tag_subject( lustre@server_component:subject(Component@1), <<"__spectator_internal Server Component"/utf8>> ), _pipe = lustre@server_component:register_subject(Self), gleam@erlang@process:send(Component@1, _pipe), Selector = begin _pipe@1 = gleam_erlang_ffi:new_selector(), gleam@erlang@process:select(_pipe@1, Self) end, {{socket, Component@1, Self}, {some, Selector}} end, Socket_update = fun(State, Msg, Conn) -> case Msg of {text, Json} -> Action = gleam@json:parse( Json, lustre@server_component:runtime_message_decoder() ), case Action of {ok, Action@1} -> gleam@erlang@process:send( erlang:element(2, State), Action@1 ); {error, _} -> nil end, mist:continue(State); {binary, _} -> mist:continue(State); {custom, Patch} -> case begin _pipe@2 = Patch, _pipe@3 = lustre@server_component:client_message_to_json( _pipe@2 ), _pipe@4 = gleam@json:to_string(_pipe@3), mist:send_text_frame(Conn, _pipe@4) end of {ok, _} -> nil; _assert_fail@1 -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <<?FILEPATH/utf8>>, module => <<"spectator"/utf8>>, function => <<"connect_server_component"/utf8>>, line => 387, value => _assert_fail@1, start => 11801, 'end' => 11961, pattern_start => 11812, pattern_end => 11817}) end, mist:continue(State); closed -> mist:stop(); shutdown -> mist:stop() end end, Socket_close = fun(State@1) -> _pipe@5 = lustre@server_component:deregister_subject( erlang:element(3, State@1) ), gleam@erlang@process:send(erlang:element(2, State@1), _pipe@5), _pipe@6 = lustre:shutdown(), gleam@erlang@process:send(erlang:element(2, State@1), _pipe@6) end, mist:websocket(Req, Socket_update, Socket_init, Socket_close). -file("src/spectator.gleam", 42). -spec mist_supervised(binary(), integer()) -> gleam@otp@supervision:child_specification(gleam@otp@static_supervisor:supervisor()). mist_supervised(Host, Port) -> Empty_body = {bytes, gleam@bytes_tree:new()}, Not_found = gleam@http@response:set_body( gleam@http@response:new(404), Empty_body ), _pipe@14 = fun(Req) -> Query_params = begin _pipe = gleam@http@request:get_query(Req), gleam@result:unwrap(_pipe, []) end, case gleam@http@request:path_segments(Req) of [<<"dashboard"/utf8>>] -> render_server_component( <<"Dashboard"/utf8>>, <<"dashboard-feed"/utf8>>, Query_params ); [<<"processes"/utf8>>] -> render_server_component( <<"Processes"/utf8>>, <<"process-feed"/utf8>>, Query_params ); [<<"ets"/utf8>>] -> render_server_component( <<"ETS"/utf8>>, <<"ets-feed"/utf8>>, Query_params ); [<<"ets"/utf8>>, Table] -> render_server_component( <<"ETS"/utf8>>, <<"ets-feed/"/utf8, Table/binary>>, Query_params ); [<<"ports"/utf8>>] -> render_server_component( <<"Ports"/utf8>>, <<"port-feed"/utf8>>, Query_params ); [<<"dashboard-feed"/utf8>>] -> connect_server_component( Req, fun spectator@internal@components@dashboard_live:app/0, Query_params ); [<<"process-feed"/utf8>>] -> connect_server_component( Req, fun spectator@internal@components@processes_live:app/0, Query_params ); [<<"ets-feed"/utf8>>] -> connect_server_component( Req, fun spectator@internal@components@ets_overview_live:app/0, Query_params ); [<<"ets-feed"/utf8>>, Table@1] -> connect_server_component( Req, fun spectator@internal@components@ets_table_live:app/0, [{<<"table_name"/utf8>>, begin _pipe@1 = gleam_stdlib:percent_decode(Table@1), gleam@result:unwrap(_pipe@1, <<""/utf8>>) end} | Query_params] ); [<<"port-feed"/utf8>>] -> connect_server_component( Req, fun spectator@internal@components@ports_live:app/0, Query_params ); [<<"favicon.svg"/utf8>>] -> Priv@1 = case gleam_erlang_ffi:priv_directory( <<"spectator"/utf8>> ) of {ok, Priv} -> Priv; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <<?FILEPATH/utf8>>, module => <<"spectator"/utf8>>, function => <<"mist_supervised"/utf8>>, line => 78, value => _assert_fail, start => 2808, 'end' => 2869, pattern_start => 2819, pattern_end => 2827}) end, Path = <<Priv@1/binary, "/lucy_spectator.svg"/utf8>>, _pipe@2 = mist:send_file(Path, 0, none), _pipe@5 = gleam@result:map( _pipe@2, fun(Favicon) -> _pipe@3 = gleam@http@response:new(200), _pipe@4 = gleam@http@response:prepend_header( _pipe@3, <<"content-type"/utf8>>, <<"image/svg+xml"/utf8>> ), gleam@http@response:set_body(_pipe@4, Favicon) end ), gleam@result:lazy_unwrap( _pipe@5, fun() -> _pipe@6 = gleam@http@response:new(404), gleam@http@response:set_body( _pipe@6, {bytes, gleam@bytes_tree:new()} ) end ); [<<"connect-widget.js"/utf8>>] -> Priv@3 = case gleam_erlang_ffi:priv_directory( <<"spectator"/utf8>> ) of {ok, Priv@2} -> Priv@2; _assert_fail@1 -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <<?FILEPATH/utf8>>, module => <<"spectator"/utf8>>, function => <<"mist_supervised"/utf8>>, line => 92, value => _assert_fail@1, start => 3350, 'end' => 3411, pattern_start => 3361, pattern_end => 3369}) end, Path@1 = <<Priv@3/binary, "/connect-widget.js"/utf8>>, _pipe@7 = mist:send_file(Path@1, 0, none), _pipe@10 = gleam@result:map( _pipe@7, fun(Script) -> _pipe@8 = gleam@http@response:new(200), _pipe@9 = gleam@http@response:prepend_header( _pipe@8, <<"content-type"/utf8>>, <<"application/javascript"/utf8>> ), gleam@http@response:set_body(_pipe@9, Script) end ), gleam@result:lazy_unwrap( _pipe@10, fun() -> _pipe@11 = gleam@http@response:new(404), gleam@http@response:set_body( _pipe@11, {bytes, gleam@bytes_tree:new()} ) end ); [] -> _pipe@12 = gleam@http@response:new(302), _pipe@13 = gleam@http@response:prepend_header( _pipe@12, <<"location"/utf8>>, <<"/dashboard"/utf8>> ), gleam@http@response:set_body(_pipe@13, Empty_body); _ -> Not_found end end, _pipe@15 = mist:new(_pipe@14), _pipe@17 = mist:after_start( _pipe@15, fun(Port@1, Scheme, Interface) -> Address = case Interface of {ip_v6, _, _, _, _, _, _, _, _} -> <<<<"["/utf8, (mist:ip_address_to_string(Interface))/binary>>/binary, "]"/utf8>>; _ -> mist:ip_address_to_string(Interface) end, Message = <<<<<<<<<<<<<<"🔍 Spectator is listening on "/utf8, (gleam@http:scheme_to_string(Scheme))/binary>>/binary, "://"/utf8>>/binary, Address/binary>>/binary, ":"/utf8>>/binary, (erlang:integer_to_binary(Port@1))/binary>>/binary, " - Node: "/utf8>>/binary, (erlang:atom_to_binary( begin _pipe@16 = erlang:node(), gleam_erlang_ffi:identity(_pipe@16) end ))/binary>>, gleam_stdlib:println(Message) end ), _pipe@18 = mist:port(_pipe@17, Port), _pipe@19 = mist:bind(_pipe@18, Host), mist:supervised(_pipe@19). -file("src/spectator.gleam", 142). -spec start_on(binary(), integer()) -> {ok, gleam@otp@actor:started(gleam@otp@static_supervisor:supervisor())} | {error, gleam@otp@actor:start_error()}. start_on(Host, Port) -> _pipe = gleam@otp@static_supervisor:new(one_for_one), _pipe@1 = gleam@otp@static_supervisor:add( _pipe, gleam@otp@supervision:worker( fun() -> case spectator_tag_manager:start_link() of {ok, Pid} -> {ok, {started, Pid, nil}}; {error, Error} -> {error, {init_failed, <<"Failed to start tag manager: "/utf8, (gleam@string:inspect(Error))/binary>>}} end end ) ), _pipe@2 = gleam@otp@static_supervisor:add( _pipe@1, mist_supervised(Host, Port) ), gleam@otp@static_supervisor:start(_pipe@2). -file("src/spectator.gleam", 138). ?DOC(" Start the spectator application on port 3000\n"). -spec start() -> {ok, gleam@otp@actor:started(gleam@otp@static_supervisor:supervisor())} | {error, gleam@otp@actor:start_error()}. start() -> start_on(<<"127.0.0.1"/utf8>>, 3000). -file("src/spectator.gleam", 37). ?DOC( " Entrypoint for running spectator from the command line.\n" " This will start the spectator application on port 3000 and never return.\n" ). -spec main() -> nil. main() -> case start() of {ok, _} -> nil; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <<?FILEPATH/utf8>>, module => <<"spectator"/utf8>>, function => <<"main"/utf8>>, line => 38, value => _assert_fail, start => 1200, 'end' => 1226, pattern_start => 1211, pattern_end => 1216}) end, gleam_erlang_ffi:sleep_forever().