-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(AFMB) :: {socket, lustre:runtime(AFMB), gleam@erlang@process:subject(lustre@runtime@transport:client_message(AFMB))}. -file("src/spectator.gleam", 142). ?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", 149). ?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(AFMI), binary()) -> gleam@erlang@process:subject(AFMI). 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 => 153, value => _assert_fail, start => 4852, 'end' => 4899, pattern_start => 4863, pattern_end => 4870}) end, tag(Pid@1, Name), Sub. -file("src/spectator.gleam", 160). ?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(AFML)} | {error, AFMN}, binary() ) -> {ok, gleam@erlang@process:subject(AFML)} | {error, AFMN}. tag_result(Result, Name) -> case Result of {ok, Sub} -> {ok, tag_subject(Sub, Name)}; Other -> Other end. -file("src/spectator.gleam", 176). -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", 219). -spec serve_string(binary()) -> gleam@http@response:response(mist:response_data()). serve_string(Content) -> _pipe = gleam@http@response:new(200), gleam@http@response:set_body( _pipe, {bytes, gleam_stdlib:wrap_list(Content)} ). -file("src/spectator.gleam", 224). -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", 228). -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 = <<"/* Gloal */\n* {\n font-family: Arial, sans-serif;\n font-size: 14px;\n box-sizing: border-box;\n}\n\n:root {\n /* Default Theme */\n --text: #ffffff;\n --muted: #595959;\n --background: #2f2f2f;\n --highlight: #584355;\n --text-hightlight: #ffaff3;\n\n /* General */\n --unnamed-blue: #a6f0fc;\n --meter-bar: #ffaff3;\n}\n\n@media (prefers-color-scheme: light) {\n :root {\n --text: #2f2f2f;\n --muted: #d2d2d2;\n --background: #ffffff;\n --highlight: #fffbe8;\n --text-hightlight: rgb(171, 98, 160);\n }\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --text: #ffffff;\n --muted: #595959;\n --background: #2f2f2f;\n --highlight: #584355;\n --text-hightlight: #ffaff3;\n }\n}\n\n*::selection {\n background: var(--text);\n color: var(--background);\n}\n\n*::-moz-selection {\n background: var(--text);\n color: var(--background);\n}\n\ninput:focus {\n outline: none;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n color: var(--text);\n background: var(--background);\n}\n\nbody {\n overflow-y: scroll;\n}\n\ndl {\n margin: 5px;\n}\n\ndt {\n font-weight: bold;\n margin-bottom: 5px;\n}\n\nbutton,\n.button {\n background: var(--background);\n border: 1px solid var(--text);\n color: var(--text);\n padding: 1px 4px;\n text-decoration: none;\n cursor: pointer;\n}\n\ndialog {\n border: 1px solid var(--text);\n background: var(--background);\n color: var(--text);\n}\n\ninput{\n background: var(--background);\n border: 1px solid var(--text);\n color: var(--text);\n padding: 2px 4px;\n}\n\nbutton:hover,\n.button:hover {\n background: var(--text);\n color: var(--background);\n}\n\n.interactive-primitive {\n cursor: pointer;\n background: unset;\n color: var(--text);\n text-decoration: underline;\n border: 0;\n padding: 0;\n}\n\n.interactive-primitive:hover {\n background: var(--text);\n color: var(--background);\n}\n\n.interactive-primitive.block {\n display: block;\n}\n\n.interactive-primitive.tagged {\n color: var(--text-hightlight);\n}\n\n.interactive-primitive.tagged:hover {\n color: var(--background);\n background: var(--text-hightlight);\n}\n\n/* Top Navigational Bar */\n.topbar {\n display: flex;\n font-weight: 100;\n align-items: flex-end;\n justify-content: space-between;\n user-select: none;\n}\n\n.lucy-icon {\n width: 3em;\n}\n\n.logo {\n font-size: 28px;\n display: flex;\n align-items: center;\n padding: 5px 2.5px;\n border-bottom: 1px solid var(--text);\n}\n\n.filler {\n height: 100%;\n flex-grow: 2;\n border-bottom: 1px solid var(--text);\n}\n\n.separator {\n height: 100%;\n width: 5px;\n border-bottom: 1px solid var(--text);\n}\n\n.tabs {\n display: flex;\n align-items: end;\n}\n\n.tab {\n display: flex;\n align-items: center;\n border: 1px solid var(--text);\n padding: 10px;\n border-top-right-radius: 10px;\n border-top-left-radius: 10px;\n user-select: none;\n cursor: pointer;\n text-decoration: none;\n color: var(--text);\n}\n\n.tab.disabled {\n cursor: not-allowed;\n}\n\n.tab.active {\n border-bottom: 1px solid var(--background);\n}\n\n.tab > svg,\n.tab > div {\n height: 3em;\n}\n\n.connection-status {\n border-bottom: 1px solid var(--text);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n gap: 10px;\n}\n\n.change-target-button {\n display: none;\n}\n\n/* Table View */\ntable {\n width: 100%;\n min-height: 100vh;\n border-spacing: 0;\n}\n\nthead {\n position: sticky;\n top: 0;\n left: 0;\n z-index: 1;\n background: var(--background);\n user-select: none;\n}\n\ntbody {\n user-select: none;\n}\n\nthead th {\n border-bottom: 1px solid var(--text);\n}\n\nth:hover {\n background: var(--highlight);\n}\n\nth div {\n display: flex;\n align-items: center;\n}\n\nth div svg {\n margin-right: 5px;\n}\n\ntfoot {\n position: sticky;\n bottom: 0;\n left: 0;\n background: var(--background);\n}\n\ntfoot td {\n border-top: 1px solid var(--text);\n}\n\nth:first-child {\n border-left: 0;\n}\n\n.buffer-row {\n pointer-events: none;\n height: auto;\n}\n\n.buffer-row > td {\n height: auto;\n}\n\nth,\ntd {\n text-align: left;\n padding: 2.5px;\n border: 0;\n max-width: 20vw;\n height: 1.6em;\n text-overflow: ellipsis;\n overflow: hidden;\n}\n\ntd.link-cell {\n padding: 0;\n}\n\ntd.link-cell > a {\n display: block;\n padding: 2.5px;\n}\n\ntd.cell-right,\nth.cell-right {\n text-align: right;\n}\n\ntd > a {\n text-decoration: none;\n color: var(--text);\n display: block;\n}\n\ntbody td:first-of-type,\nthead th:first-of-type {\n padding-left: 5px;\n}\n\ntbody td:last-of-type,\nthead th:last-of-type {\n padding-right: 5px;\n}\n\ntfoot th,\ntfoot td {\n padding: 0;\n}\n\ntbody tr:hover {\n background: var(--highlight);\n}\n\ntbody tr.selected {\n background: var(--text);\n color: var(--background);\n}\n\ntbody tr.selected .interactive-primitive {\n color: var(--background);\n}\n\ntbody tr.tagged {\n color: var(--text-hightlight);\n}\n\n.spectator-tagged {\n opacity: 0.5;\n}\n\ntbody tr.tagged.selected {\n background: var(--text-hightlight);\n color: var(--background);\n}\n\n.component-error {\n width: 100%;\n height: 50vh;\n display: flex;\n justify-content: center;\n align-items: center;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n/* Process Details View */\n.details {\n display: grid;\n grid-template-columns: 1fr 1fr;\n grid-template-rows: auto 1fr;\n width: 100vw;\n overflow: hidden;\n}\n\n.details > div {\n height: 250px;\n overflow: auto;\n overscroll-behavior: none;\n}\n\n.panel-heading {\n padding: 5px;\n border-bottom: 1px solid var(--text);\n position: sticky;\n top: 0;\n left: 0;\n background: var(--background);\n box-sizing: content-box;\n height: 1.2em;\n display: flex;\n align-items: center;\n grid-column: span 2;\n}\n\n.panel-content {\n padding: 5px;\n overflow: auto;\n}\n\n.details .general {\n border-right: 1px solid var(--text);\n}\n\n.footer-placeholder {\n padding: 2px;\n text-align: right;\n}\n\n.panel-action {\n margin-left: auto;\n cursor: pointer;\n}\n\n/* ETS */\n\ntable.ets-data td {\n border-bottom: 1px solid var(--muted);\n user-select: text;\n}\n\ntable.ets-data tbody tr:hover {\n background: var(--background);\n}\n\n/* Ports */\n\n.details.compact {\n display: flex;\n}\n\n.details.compact > dl {\n flex-basis: 33%;\n}\n\n.details.compact dt {\n text-align: left;\n}\n\n.details.compact dd {\n margin-left: 0;\n}\n\n/* Dashboard */\n\n.dashboard {\n padding: 10px;\n}\n\n.split {\n display: flex;\n gap: 100px;\n width: 100%;\n}\n\n.split > div {\n flex-basis: 50%;\n}\n\n.info-container {\n border: 1px solid var(--text);\n}\n\n.info-item {\n display: flex;\n}\n\n.info-item {\n border-bottom: 1px solid var(--text);\n}\n\n.info-item:last-child {\n border-bottom: 0;\n}\n\n.info-item > * {\n flex-basis: 50%;\n padding: 5px;\n border: 0;\n background: var(--background);\n color: var(--text);\n}\n\n.info-item > a {\n text-align: center;\n user-select: none;\n}\n\n.info-item > a:first-child {\n border-right: 1px solid var(--text);\n}\n\n.info-label {\n border-right: 1px solid var(--text);\n text-align: right;\n}\n\n.column-chart,\n.meter-chart {\n padding: 2px;\n border: 1px solid var(--text);\n}\n\n.memory-breakdown {\n display: flex;\n gap: 10px;\n}\n\n.limit-item {\n margin-bottom: 10px;\n}\n\n.legend-item {\n display: flex;\n}\n\n.legend-colour {\n width: 2em;\n height: 2em;\n margin-right: 10px;\n padding: 2px;\n border: 1px solid var(--text);\n display: flex;\n}\n\n.legend-colour > div {\n flex-grow: 1;\n}\n\n.connect-widget-content h2 {\n margin-top: 0;\n}\n\n.connect-widget-form {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.connect-widget-actions {\n margin-top: 15px;\n}\n"/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( [], <