From 4712cc76ae9be743b054bdbef487c008ce691f30 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Wed, 24 Jun 2026 19:54:41 +0200 Subject: [PATCH] feat: receiver shows full message as a structured record card Expand every field of the intercepted message into a readable card instead of just the inner payload: from (email/phone), timestamp, message body (text or clickable attachment link), and a metadata row (type/action, message id, chat id, sender, to, user-agent). - ldf_www_controller: decode the stored message and expand all fields (fmt_time via rfc3339, short uuids); fall back to raw text if not JSON - receiver feed is a list of .rec cards (div), not a table - ldf.css: intercept-record card styling Verified in headless Chrome. --- priv/assets/css/ldf.css | 16 +++++ src/controllers/ldf_www_controller.erl | 90 ++++++++++++++++++-------- src/views/message_row.dtl | 2 +- src/views/receiver.dtl | 9 +-- 4 files changed, 83 insertions(+), 34 deletions(-) diff --git a/priv/assets/css/ldf.css b/priv/assets/css/ldf.css index b576df3..5de2ef8 100644 --- a/priv/assets/css/ldf.css +++ b/priv/assets/css/ldf.css @@ -134,6 +134,22 @@ tbody tr:last-child td { border-bottom: none; } } tbody tr.new { animation: flashin .7s ease-out; } +/* intercept records (receiver feed) */ +.records { display: flex; flex-direction: column; } +.rec { padding: 14px 18px; border-bottom: 1px solid var(--line-soft); } +.rec:last-child { border-bottom: none; } +.rec.new { animation: flashin .7s ease-out; } +.rec-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 8px; } +.rec-from { font-family: var(--sans); font-weight: 600; font-size: 13px; color: var(--text); } +.rec-phone { color: var(--amber); font-weight: 400; font-size: 12px; margin-left: 8px; } +.rec-time { margin-left: auto; color: var(--faint); font-size: 11px; white-space: nowrap; } +.rec-body { color: var(--text); font-size: 13px; margin-bottom: 10px; word-break: break-word; padding-left: 10px; border-left: 2px solid var(--line); } +.rec-body a { color: var(--live); text-decoration: none; border-bottom: 1px dotted var(--live-dim); word-break: break-all; } +.rec-body a:hover { border-bottom-style: solid; } +.rec-meta { display: flex; flex-wrap: wrap; gap: 5px 18px; font-size: 10.5px; color: var(--dim); } +.rec-meta i { color: var(--faint); font-style: normal; text-transform: uppercase; letter-spacing: .08em; margin-right: 6px; } +.rec-meta .rec-agent { flex-basis: 100%; color: var(--faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .empty { padding: 40px 18px; text-align: center; color: var(--faint); font-size: 12.5px; } .empty::before { content: "▱ "; color: var(--live-dim); } diff --git a/src/controllers/ldf_www_controller.erl b/src/controllers/ldf_www_controller.erl index f527ab9..068bfcf 100644 --- a/src/controllers/ldf_www_controller.erl +++ b/src/controllers/ldf_www_controller.erl @@ -77,8 +77,8 @@ submit_history(Req0) -> %% --- published by the message API on each intercepted message ------------- -publish_message(MessageId, Payload) -> - Row = render(message_row_dtl, row_vars(ref(MessageId), Payload, true)), +publish_message(_MessageId, Payload) -> + Row = render(message_row_dtl, row_vars(Payload, true)), datastar_nova:publish(?MESSAGES, [ datastar:patch_elements(~"", #{selector => ~"#feed-empty", mode => remove}), datastar:patch_elements(Row, #{selector => ~"#messages", mode => append}) @@ -91,35 +91,75 @@ publish_message(MessageId, Payload) -> message_init([]) -> [datastar:patch_elements(feed_empty(), #{selector => ~"#messages", mode => inner})]; message_init(Msgs) -> - Rows = [ - render( - message_row_dtl, - row_vars(ref(maps:get(message_id, M, ~"")), maps:get(payload, M, ~""), false) - ) - || M <- Msgs - ], + Rows = [render(message_row_dtl, row_vars(maps:get(payload, M, ~""), false)) || M <- Msgs], [datastar:patch_elements(Rows, #{selector => ~"#messages", mode => inner})]. feed_empty() -> - ~"Awaiting interception". - -%% The stored payload is the full encoded message; show its inner payload - -%% an attachment URL becomes a clickable link, plain text stays text. -row_vars(Ref, StoredPayload, New) -> - case display_payload(StoredPayload) of - {link, Url} -> [{ref, Ref}, {new, New}, {link, Url}, {text, ~""}]; - {text, Text} -> [{ref, Ref}, {new, New}, {link, false}, {text, Text}] + ~"
Awaiting interception
". + +%% The stored payload is the full encoded message; expand every field for a +%% structured record. The inner payload renders as text, or a clickable link +%% for attachments. +row_vars(StoredPayload, New) -> + case safe_decode(StoredPayload) of + Msg when is_map(Msg) -> + [{new, New} | fields(Msg)]; + _ -> + [{new, New}, {text, StoredPayload}, {link, false} | blank_fields()] end. -display_payload(StoredPayload) -> - try json:decode(StoredPayload) of - #{~"payload" := #{~"url" := Url}} when is_binary(Url) -> {link, public_url(Url)}; - #{~"payload" := Text} when is_binary(Text) -> {text, Text}; - _ -> {text, StoredPayload} +fields(Msg) -> + SenderInfo = maps:get(~"sender_info", Msg, #{}), + Payload = + case maps:get(~"payload", Msg, ~"") of + #{~"url" := Url} when is_binary(Url) -> [{link, public_url(Url)}, {text, ~""}]; + Text when is_binary(Text) -> [{link, false}, {text, Text}]; + _ -> [{link, false}, {text, ~""}] + end, + [ + {email, maps:get(~"email", SenderInfo, ~"")}, + {phone, maps:get(~"phone_number", SenderInfo, ~"")}, + {agent, maps:get(~"user_agent", SenderInfo, ~"")}, + {time, fmt_time(maps:get(~"timestamp", Msg, undefined))}, + {type, maps:get(~"type", Msg, ~"")}, + {action, maps:get(~"action", Msg, ~"")}, + {sender, short(maps:get(~"sender", Msg, ~""))}, + {recipient, short(maps:get(~"to", Msg, ~""))}, + {chat, short(maps:get(~"chat_id", Msg, ~""))}, + {id, short(maps:get(~"id", Msg, ~""))} + | Payload + ]. + +blank_fields() -> + [ + {email, ~""}, + {phone, ~""}, + {agent, ~""}, + {time, ~""}, + {type, ~""}, + {action, ~""}, + {sender, ~""}, + {recipient, ~""}, + {chat, ~""}, + {id, ~""} + ]. + +safe_decode(Bin) -> + try json:decode(Bin) of + Decoded -> Decoded catch - _:_ -> {text, StoredPayload} + _:_ -> error end. +fmt_time(Ms) when is_integer(Ms) -> + list_to_binary(calendar:system_time_to_rfc3339(Ms div 1000, [{offset, "Z"}])); +fmt_time(_) -> + ~"". + +short(<>) -> Eight; +short(Bin) when is_binary(Bin) -> Bin; +short(_) -> ~"". + %% Rewrite an attachment URL onto the browser-reachable chatli host, %% regardless of which host got baked in when it was stored. public_url(Url) -> @@ -155,7 +195,3 @@ status_patch(Text) -> render(Mod, Vars) -> {ok, Html} = Mod:render(Vars), string:trim(iolist_to_binary(Html), trailing). - -ref(<>) -> Short; -ref(MessageId) when is_binary(MessageId) -> MessageId; -ref(_) -> ~"". diff --git a/src/views/message_row.dtl b/src/views/message_row.dtl index 73c29b1..4a1353f 100644 --- a/src/views/message_row.dtl +++ b/src/views/message_row.dtl @@ -1 +1 @@ -{{ ref }}{% if link %}{{ link }}{% else %}{{ text }}{% endif %} +
{% if email %}{{ email }}{% endif %}{% if phone %}{{ phone }}{% endif %}{% if not email %}{% if not phone %}unidentified{% endif %}{% endif %}{{ time }}
{% if link %}{{ link }}{% else %}{{ text }}{% endif %}
type{{ type }}{% if action %} / {{ action }}{% endif %}msg{{ id }}chat{{ chat }}sender{{ sender }}to{{ recipient }}{% if agent %}agent{{ agent }}{% endif %}
diff --git a/src/views/receiver.dtl b/src/views/receiver.dtl index 9593a41..cfbd11c 100644 --- a/src/views/receiver.dtl +++ b/src/views/receiver.dtl @@ -10,12 +10,9 @@ SSE · /sse/receiver
- - - - - -
ReferencePayload
Awaiting interception
+
+
Awaiting interception
+
{% endblock %}