From 650f44cafaf6f248a6bd5ff97e723d56fac7145c Mon Sep 17 00:00:00 2001 From: Dhamari Trice-Hanson <39872667+dhamariT@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:37:31 -0400 Subject: [PATCH 1/2] Link a finished timelapse to a devlog (reassure + attach) Users thought their Lookout recording vanished: after recording, the timelapse was saved on Stardance's side but never surfaced again and was tied to nothing. The backend already supported linking a session to a devlog (Projects::DevlogsController#attach_lookout_sessions reads params[:post_devlog][:lookout_session_ids]) but nothing in the UI ever sent it, and devlogs never showed a timelapse. Wire it up (flag: timelapse_devlog, off by default): - Recorder done screen: reassure ('Saved! We've got your timelapse') and turn 'Post a Devlog' into 'Add it to a devlog', carrying the session id + just_recorded=1 to the project page. - Project page: ProjectsController#show reads lookout_session_id and sets @pending_lookout_session (attachable, owned by the user, not yet linked). - Composer: renders a hidden post_devlog[lookout_session_ids][] field for that session + a reassurance banner ('Your timelapse (Xm) is saved and will be attached to this devlog'). The modal controller auto-opens the composer on ?just_recorded=1 and strips the params. - Devlog card: shows a 'Timelapse attached' chip when the devlog has a linked session, so the connection is visible on the post. The actual timelapse frame as the devlog's image is the fast follow. Note: the card chip does a per-card lookout_sessions.exists? (gated behind the flag to bound cost during rollout); preload when this goes wide. --- app/assets/stylesheets/components/_feed.scss | 30 +++++++++++++++++++ app/components/posts/card_component.html.erb | 6 ++++ .../posts/composer_component.html.erb | 8 +++++ app/components/posts/composer_component.rb | 5 ++-- app/controllers/projects_controller.rb | 8 +++++ .../controllers/modal_controller.js | 23 ++++++++++++++ .../projects/lookout_sessions/record.html.erb | 11 +++++-- app/views/projects/show.html.erb | 3 +- config/initializers/flipper.rb | 1 + 9 files changed, 90 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/components/_feed.scss b/app/assets/stylesheets/components/_feed.scss index 4d1daab9d..9e8245b7d 100644 --- a/app/assets/stylesheets/components/_feed.scss +++ b/app/assets/stylesheets/components/_feed.scss @@ -299,6 +299,19 @@ background: var(--color-brand-salmon); } + // Reassurance banner shown when a just-finished timelapse is being carried + // into this devlog (Lookout recording → "Add it to a devlog"). + &__timelapse-note { + margin: 0 0 var(--space-xs); + padding: var(--space-xs); + border: 1px solid var(--color-brand-mint); + border-radius: var(--border-radius); + background: var(--color-space-accent-soft); + color: var(--color-space-text); + font-size: calc(var(--font-size-s) * 1.1); + line-height: 1.4; + } + &__form { display: flex; flex-direction: column; @@ -841,6 +854,23 @@ font-size: calc(var(--font-size-s) * 1.2); } + &__timelapse { + display: inline-flex; + align-items: center; + gap: var(--space-xxs); + margin: var(--space-xxs) 0 0; + font-size: calc(var(--font-size-s) * 1.1); + color: var(--color-brand-mint); + } + + &__timelapse-dot { + flex: 0 0 auto; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--color-brand-mint); + } + &__body { padding-left: var(--feed-content-offset); font-size: calc(var(--font-size-s) * 1.2); diff --git a/app/components/posts/card_component.html.erb b/app/components/posts/card_component.html.erb index 8f6458ad3..09883b887 100644 --- a/app/components/posts/card_component.html.erb +++ b/app/components/posts/card_component.html.erb @@ -57,6 +57,12 @@

<%= helpers.format_seconds(display_postable.duration_seconds) %> logged

<% end %> + <% if devlog? && current_user && Flipper.enabled?(:timelapse_devlog, current_user) && display_postable.lookout_sessions.exists? %> +

+ Timelapse attached +

+ <% end %> + <% if quote_repost? && body.present? %>
<%= helpers.md(body, allow_images: false) %> diff --git a/app/components/posts/composer_component.html.erb b/app/components/posts/composer_component.html.erb index 8c5665d42..0946433fa 100644 --- a/app/components/posts/composer_component.html.erb +++ b/app/components/posts/composer_component.html.erb @@ -37,6 +37,14 @@ action: class_names("submit->composer#guardSubmit", "paste->composer#paste": show_attachments?) } }, class: "feed-composer__form", local: true do |f| %> + <% if pending_lookout_session.present? %> + <%# Carried over from a just-finished Lookout recording: link that + session to this devlog, and reassure the user we kept their lapse. %> + <%= hidden_field_tag "post_devlog[lookout_session_ids][]", pending_lookout_session.id %> +
+ Your timelapse (<%= helpers.format_seconds(pending_lookout_session.duration_seconds) %>) is saved and will be attached to this devlog. +
+ <% end %>
<%= image_tag current_user.avatar, alt: "", class: "feed-composer__avatar" %> diff --git a/app/components/posts/composer_component.rb b/app/components/posts/composer_component.rb index 50d0dce81..acd3169df 100644 --- a/app/components/posts/composer_component.rb +++ b/app/components/posts/composer_component.rb @@ -7,13 +7,13 @@ class ComposerComponent < ViewComponent::Base attr_reader :post, :current_user, :projects, :selected_project, :test_time_granted, :url, :scope, :aria_label, :body_label, :placeholder, :submit_text, :disable_with, :simple_mode, :show_project_chips, :show_attachments, - :show_time_preview, :show_record, :quote_preview_post + :show_time_preview, :show_record, :quote_preview_post, :pending_lookout_session def initialize(post:, current_user:, projects:, selected_project:, test_time_granted: false, url: nil, scope: nil, aria_label: "Create a devlog", body_label: "What are you working on?", placeholder: "What are you working on?", submit_text: "Post", disable_with: "Posting...", simple_mode: false, show_project_chips: true, show_attachments: true, show_time_preview: true, - show_record: false, quote_preview_post: nil) + show_record: false, quote_preview_post: nil, pending_lookout_session: nil) @post = post @current_user = current_user @projects = projects @@ -32,6 +32,7 @@ def initialize(post:, current_user:, projects:, selected_project:, test_time_gra @show_time_preview = show_time_preview @show_record = show_record @quote_preview_post = quote_preview_post + @pending_lookout_session = pending_lookout_session end # When a post to quote is supplied, the composer shows it below the text box diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ab631ad71..48c107613 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -49,6 +49,14 @@ def prepare_project_show_context @composer_devlog = Post::Devlog.new @composer_projects = current_user.projects.order(updated_at: :desc) + # Coming back from a just-finished Lookout recording: pre-link that + # session to the devlog the user is about to write, so their timelapse + # attaches to the post instead of feeling lost. + if Flipper.enabled?(:timelapse_devlog, current_user) && params[:lookout_session_id].present? + @pending_lookout_session = @project.lookout_sessions.attachable + .find_by(id: params[:lookout_session_id], user: current_user, devlog_id: nil) + end + @hackatime_linked = current_user.hackatime_identity.present? if @hackatime_linked diff --git a/app/javascript/controllers/modal_controller.js b/app/javascript/controllers/modal_controller.js index ce1ddf032..f91defd49 100644 --- a/app/javascript/controllers/modal_controller.js +++ b/app/javascript/controllers/modal_controller.js @@ -30,6 +30,7 @@ export default class extends Controller { this.openSettingsModalFromQueryParam(); this.openIdvModalFromQueryParam(); + this.openComposerModalFromQueryParam(); } disconnect() { @@ -130,4 +131,26 @@ export default class extends Controller { }`; window.history.replaceState(window.history.state, "", nextUrl); } + + // Landing on the project page straight from a finished Lookout recording + // (?just_recorded=1): pop the devlog composer open so the timelapse can be + // attached, then strip the params so a refresh doesn't reopen it. + openComposerModalFromQueryParam() { + if (!this.element.id.startsWith("composer-modal-")) return; + + const params = new URLSearchParams(window.location.search); + if (!params.has("just_recorded")) return; + + if (!this.element.open) { + this.element.showModal(); + } + + params.delete("just_recorded"); + params.delete("lookout_session_id"); + const query = params.toString(); + const nextUrl = `${window.location.pathname}${query ? `?${query}` : ""}${ + window.location.hash + }`; + window.history.replaceState(window.history.state, "", nextUrl); + } } diff --git a/app/views/projects/lookout_sessions/record.html.erb b/app/views/projects/lookout_sessions/record.html.erb index 13991bcab..5b0e021d8 100644 --- a/app/views/projects/lookout_sessions/record.html.erb +++ b/app/views/projects/lookout_sessions/record.html.erb @@ -165,8 +165,15 @@ <%# Shown after the time is sent — nudge to actually log it in a devlog. %> <%= link_to "Back to project →", project_path(@project), diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index e3c8bb0a8..1585225f3 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -905,7 +905,8 @@ current_user: current_user, projects: @composer_projects, selected_project: @project, - test_time_granted: @test_time_granted + test_time_granted: @test_time_granted, + pending_lookout_session: @pending_lookout_session ) %> <% end %> diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb index 4bd1f6e66..6bdbdf01a 100644 --- a/config/initializers/flipper.rb +++ b/config/initializers/flipper.rb @@ -42,6 +42,7 @@ ship_event_payouts lookout payout_recommendations + timelapse_devlog ].each { |flag| Flipper.add(flag) } end rescue StandardError => e From eb3dd94a47d8df7a978306f35bfaa98744dd05f8 Mon Sep 17 00:00:00 2001 From: Dhamari Trice-Hanson <39872667+dhamariT@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:50:50 -0400 Subject: [PATCH 2/2] =?UTF-8?q?Fix=20timelapse=E2=86=92devlog=20link=20gap?= =?UTF-8?q?s=20flagged=20in=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Broaden the pending-session lookup to a new :linkable scope (stopped/compiling/complete) so a background sync flipping a just-stopped session to compiling mid-flow no longer silently drops the attach. Leaves :attachable (finished-video only) intact for the hardware-review recordings list. - Keep lookout_session_id in the URL after auto-opening the composer; only strip just_recorded. A pre-submit refresh now still resolves @pending_lookout_session instead of losing the link. - Hide the duration in the reassurance banner when it hasn't synced yet, so it no longer reads "Your timelapse (0s)". --- app/components/posts/composer_component.html.erb | 2 +- app/controllers/projects_controller.rb | 2 +- app/javascript/controllers/modal_controller.js | 5 +++-- app/models/lookout_session.rb | 7 +++++++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/components/posts/composer_component.html.erb b/app/components/posts/composer_component.html.erb index 0946433fa..4b131b651 100644 --- a/app/components/posts/composer_component.html.erb +++ b/app/components/posts/composer_component.html.erb @@ -42,7 +42,7 @@ session to this devlog, and reassure the user we kept their lapse. %> <%= hidden_field_tag "post_devlog[lookout_session_ids][]", pending_lookout_session.id %>
- Your timelapse (<%= helpers.format_seconds(pending_lookout_session.duration_seconds) %>) is saved and will be attached to this devlog. + Your timelapse<% if pending_lookout_session.duration_seconds.to_i > 0 %> (<%= helpers.format_seconds(pending_lookout_session.duration_seconds) %>)<% end %> is saved and will be attached to this devlog.
<% end %>
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 48c107613..9a8eb92c4 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -53,7 +53,7 @@ def prepare_project_show_context # session to the devlog the user is about to write, so their timelapse # attaches to the post instead of feeling lost. if Flipper.enabled?(:timelapse_devlog, current_user) && params[:lookout_session_id].present? - @pending_lookout_session = @project.lookout_sessions.attachable + @pending_lookout_session = @project.lookout_sessions.linkable .find_by(id: params[:lookout_session_id], user: current_user, devlog_id: nil) end diff --git a/app/javascript/controllers/modal_controller.js b/app/javascript/controllers/modal_controller.js index f91defd49..f6a90d454 100644 --- a/app/javascript/controllers/modal_controller.js +++ b/app/javascript/controllers/modal_controller.js @@ -134,7 +134,9 @@ export default class extends Controller { // Landing on the project page straight from a finished Lookout recording // (?just_recorded=1): pop the devlog composer open so the timelapse can be - // attached, then strip the params so a refresh doesn't reopen it. + // attached, then strip just_recorded so a refresh doesn't reopen it. + // Keep lookout_session_id: it's what the server reads to render the hidden + // attach field, so dropping it would lose the link on a pre-submit refresh. openComposerModalFromQueryParam() { if (!this.element.id.startsWith("composer-modal-")) return; @@ -146,7 +148,6 @@ export default class extends Controller { } params.delete("just_recorded"); - params.delete("lookout_session_id"); const query = params.toString(); const nextUrl = `${window.location.pathname}${query ? `?${query}` : ""}${ window.location.hash diff --git a/app/models/lookout_session.rb b/app/models/lookout_session.rb index beb5725f7..e628e1cc2 100644 --- a/app/models/lookout_session.rb +++ b/app/models/lookout_session.rb @@ -45,6 +45,13 @@ class LookoutSession < ApplicationRecord scope :for_project, ->(project) { where(project: project) } scope :attachable, -> { where(status: %w[stopped complete]) } + # Sessions that can be linked to a devlog from the recorder → composer flow. + # Broader than `attachable` (which requires a finished video): a session still + # `compiling` will finalize shortly, and the submit-side attach accepts any + # status, so the composer must surface the attach field for it too. Otherwise + # a background sync flipping stopped → compiling mid-flow silently drops the + # link. Excludes `failed` and the still-recording states. + scope :linkable, -> { where(status: %w[stopped compiling complete]) } # Sessions that might still advance — everything not yet in a terminal state. # SyncPendingLookoutSessionsJob re-polls these so a recording can finalize even # when the builder closed the recorder tab before Lookout finished compiling.