Build a Samsung TV Tune-Up App

Use Claude Code to build a local Samsung The Frame tune-up companion.

Introduction

30 Second Summary

A sluggish TV menu turns a quick evening choice into repeated waiting. The safest fixes are often buried across model-specific menus and support pages.

In this project, you will direct Claude Code to build Frame Refresh, a local companion for tuning a Samsung The Frame TV. The dashboard guides a year-specific tune-up from your first loading-time measurement to a printable report.

What You'll Build

You'll see Frame Refresh turn your TV year, checklist progress, and loading-time measurements into a clean report ready to print from Safari.

By the end of this project, you'll have:

  • Selected your TV's model year to reveal source-linked maintenance guidance and the matching Samsung defaults route.
  • Compared before-and-after measurements to see whether the Home menu became faster, stayed unchanged, or became slower.
  • Reopened the local app with saved progress restored through Web Storage when available. You'll also have a clean report ready to print or save.
  • Secret Mission: Audit enlarged zoom, keyboard operation, and the printed report like a product designer.

Are there any prerequisites?

You need a Mac with existing Claude Code 2.1.316 access. You also need a 2018 through 2022 Samsung The Frame TV with access to its remote control or power connection.

Internet access is required for TV updates and optional source links. This project adds no hosting, API, or cloud charge.

Before We Start

This moment sets the boundary for Frame Refresh before the hands-on work begins. You are building a local Safari companion that guides maintenance on your Samsung The Frame without modifying or remotely resetting the TV.

Set Up a Safe Claude Code Workspace

An agentic coding workflow can create files quickly. A dedicated folder keeps that work separate from your personal files.

Claude Code uses its starting folder as the center of its workspace. This step gives that workspace a clear home before any app files exist.

In this step, get ready to:
  • Create a dedicated project folder in Terminal.
  • Verify Claude Code 2.1.316.
  • Start a bounded Claude Code session inside the project folder.
Create a dedicated project folder

The project folder becomes the single home for every app file Claude creates. Keeping it under Documents makes the workspace boundary easy to find.

  • Press Cmd+Space to open the Mac search bar.
  • Type Terminal into the search bar.
  • Press Return to open Terminal.
  • Create the frame-refresh folder inside Documents by running this command:
mkdir -p "$HOME/Documents/frame-refresh" && cd "$HOME/Documents/frame-refresh"

What does this command do?

  • The mkdir -p portion creates the folder path if it does not already exist.
  • The $HOME/Documents/frame-refresh path places the project inside your Documents folder.
  • The cd portion moves the current Terminal session into the new folder.
  • Confirm Terminal is using the new folder by running this command:
pwd

What does this command do?

The pwd command prints the absolute path of Terminal's current folder. It gives you a direct check before Claude Code starts.

You should see a path ending in /Documents/frame-refresh.

Does the path end somewhere else?

  • Check that you copied the folder command exactly.
  • Run the folder command again from the same Terminal window.

I need help getting Terminal into my frame-refresh folder.

Start a verified Claude Code session

The project uses a specific Claude Code release so your experience matches the walkthrough. A version check confirms which release Terminal can access.

  • Check the installed Claude Code release by running this command:
claude --version

What does this command do?

The claude --version command prints the installed Claude Code release. The output also identifies the command as Claude Code.

✔️ I see the required version

Good work. Your installed Claude Code release matches this project.

The output starts with 2.1.316. It ends with (Claude Code).

ⓧ I see a different version

A different release can produce a different permission flow. Update Claude Code before continuing.

  • Open the official Claude Code quickstart.
  • Follow the current macOS installation steps on that page.
  • Return to Terminal after the installation finishes.
  • Repeat the version check above.

ⓧ Command not found

Terminal cannot access Claude Code yet. The official quickstart provides the current installation path.

  • Open the official Claude Code quickstart.
  • Complete the documented macOS installation flow.
  • Return to Terminal after the installation finishes.
  • Repeat the version check above.

The first session may display a workspace trust prompt before the chat opens. This checkpoint controls Claude Code's access to the folder.

Before you start the session, which directory do you expect Claude Code to show as its workspace?

  • Start the interactive Claude Code session by running this command:
claude

What does this command do?

The claude command starts an interactive session from Terminal's current folder. That starting location becomes the center of the project workspace.

  • Review the workspace trust prompt when it appears.
  • Continue through the default permission flow.

You should reach the Claude Code prompt with $HOME/Documents/frame-refresh shown as the working directory. This confirms the session started inside the dedicated project folder.

Does Claude show the wrong folder?

  • Enter /exit if the Claude Code prompt names a different folder.
  • Repeat the folder command from the first substep.
  • Repeat the Claude Code start command above.

Help me start Claude Code inside the correct project folder.

Set the workspace boundary

A clear instruction defines what Claude can create for this project. It also prevents package installation from adding tools you do not need.

Before you set the boundary, what two constraints do you expect Claude to repeat back?

  • Tell Claude that every file it creates must stay inside the current folder.
  • Tell Claude that it must not install packages.
  • Ask Claude to repeat both constraints before it does any project work.

Claude's response should confirm the current folder boundary. It should also confirm that no packages will be installed.

Your Claude Code session now has a clear working boundary. The project is ready for safe file creation.

Your workspace is ready. Next, you will direct Claude Code to build the first visible Frame Refresh interface.

Build the App Shell

The safe workspace from the last step gives Claude Code a clear boundary. Now you can turn that empty folder into the visible foundation of Frame Refresh.

A visible interface gives you an immediate result to review before Claude adds the maintenance logic. This shell establishes every screen region that the interactive flow needs later.

In this step, get ready to:
  • Direct Claude to create the three local app files inside the existing project folder.
  • Build a responsive Mac-inspired visual system with clear interaction states.
  • Open the local app in Safari to verify its controls and file references.
Brief Claude on the shell structure

An app shell is the page structure that holds controls and content before their interactive behavior is connected. Giving Claude exact file names and element IDs keeps later requests anchored to the same interface.

What belongs in the first brief?

The HTML file provides the page structure. The CSS file provides the visual system. The JavaScript file is the deferred entry point for later interactions.

  • Create index.html, styles.css, and app.js inside the current frame-refresh folder.
  • Keep the app local and dependency-free. Use no external packages.
  • Use original interface shapes and text. Include no Apple logos or copied assets.
  • Add the Frame Refresh brand inside an app-bar at the top of index.html.
  • Add the modelYear selector with options for 2018, 2019-2021, and 2022.
  • Add number fields with the IDs beforeTime and afterTime.
  • Add progressBar and progressText for checklist progress.
  • Add the empty taskList container for tasks that arrive in the next step.
  • Add the hidden report area with a printButton.
  • Link styles.css from index.html.
  • Load app.js from index.html with defer.
  • Switch back to the Claude Code session from the previous step.
  • Give Claude a short product brief that includes every requirement above.
  • Ask Claude to create the three files now.
  • Approve only file changes that remain inside $HOME/Documents/frame-refresh.
  • Ask Claude to report the path of each file it created.

Claude should report index.html, styles.css, and app.js inside $HOME/Documents/frame-refresh.

Did Claude create a file elsewhere?

Remind Claude that the current folder is the only allowed workspace. Ask it to move any misplaced app file into $HOME/Documents/frame-refresh before continuing.

Ask Claude to remove any external package or asset reference that appeared in its first draft.

I need help checking where Claude created my Frame Refresh files.

Shape the visual system

The visual system determines whether the shell stays readable as the window changes size. It also makes keyboard focus and system preferences visible from the first render.

What should the design communicate?

  • Use system fonts with a strong heading hierarchy.
  • Use restrained blue accents against calm neutral surfaces.
  • Use rounded cards with generous spacing.
  • Use a translucent sticky app-bar above the page content.
  • Give every button and form control a visible keyboard focus state.
  • Make the cards responsive for narrow and wide windows.
  • Respect the system light or dark appearance.
  • Respect reduced-motion preferences.
  • Include report-focused print rules for the later report flow.
  • Ask Claude to implement the visual requirements above in styles.css.
  • Ask Claude to inspect index.html for every class that the stylesheet expects.

The first browser check confirms that the files exist as a working local page. It also reveals whether the stylesheet reference resolves correctly.

  • Press Cmd+Space to open Spotlight on your Mac.
  • Enter Finder in Spotlight to launch Finder.
  • Select Documents in the Finder sidebar.
  • Open the frame-refresh folder.

You should see index.html, styles.css, and app.js listed in the folder.

  • Select index.html in Finder.
  • Click File in the macOS menu bar.
  • Select Open With.
  • Choose Safari.

That first render matters: Frame Refresh now has a polished surface you can judge before any maintenance logic arrives.

You should see the Frame Refresh brand above a rounded responsive layout. The page should also show the model selector, measurement fields, checklist area, and report structure.

Does the page look unstyled?

Ask Claude to confirm that index.html links to styles.css with the exact local filename.

Confirm that both files appear beside each other inside $HOME/Documents/frame-refresh.

Help me find why my local Frame Refresh page opens in Safari without its styles.

Add the safety controls and verify the shell

The header needs two separate reset controls because they serve different purposes. Keeping them distinct prepares the interface for a safe TV guidance panel and an independent app-progress reset.

Which controls belong in the header?

  • Add the primary Samsung defaults button with the ID tvResetButton.
  • Add the quieter Reset app button with the ID resetButton.
  • Connect tvResetButton to the hidden resetGuide dialog through its accessibility control reference.
  • Add resetBackdrop behind the dialog panel.
  • Add resetCloseButton inside the dialog panel.
  • Keep both header controls visible in the sticky app-bar.
  • Switch back to the Claude Code session from earlier.
  • Ask Claude to add every header and dialog requirement above to index.html.
  • Ask Claude to style the new controls within the existing visual system.
  • Ask Claude to inspect all three files for duplicate IDs.
  • Ask Claude to confirm that every stylesheet and script filename matches its reference in index.html.
  • Ask Claude to confirm that the visible labels match their associated form controls.

Claude should report matching references for styles.css and app.js. It should also report unique IDs for every control.

Before you reopen the page, ask yourself which controls should remain visible at the top of the finished shell.

  • Switch back to Finder.
  • Select index.html.
  • Click File in the macOS menu bar.
  • Select Open With.
  • Choose Safari.

You should see the sticky Frame Refresh header with separate Samsung defaults and Reset app controls. Below it, you should see the year selector, two measurement fields, progress area, checklist placeholder, and responsive rounded cards.

✔️ Awesome, I've got everything!

Your local app shell is open in Safari. Double check that Claude has saved all three files inside $HOME/Documents/frame-refresh.

ⓧ I'd like to double check the full code

Ask Claude to inspect the complete contents of each file. Confirm that its report matches these cumulative file states exactly.

index.html

Created as the local Frame Refresh HTML shell. It includes the app-bar, tvResetButton, resetButton, modelYear selector, beforeTime and afterTime fields, progressBar, progressText, taskList, report, printButton, and hidden resetGuide dialog with resetBackdrop and resetCloseButton. It links styles.css and app.js.

styles.css

Created as the local Frame Refresh stylesheet. It defines the responsive Mac-inspired glass-card layout, translucent sticky navigation, system typography, restrained blue accents, visible focus styles, responsive breakpoints, dark-mode variables, reduced-motion support, and print rules.

app.js

Created as the local Frame Refresh JavaScript entry file. It is loaded with defer and is reserved for the interactive tune-up flow.

Is a control missing or duplicated?

Give Claude the missing or repeated ID from its inspection report. Ask it to keep one matching element for that ID inside index.html.

Ask Claude to repeat the duplicate-ID and file-reference review after the correction.

Help me compare my Frame Refresh shell against the required element IDs.

Your app shell is now visible and ready for interaction. Next, you will turn its empty checklist and measurement fields into the first working tune-up flow.

Add the First Tune-Up Flow

The Frame Refresh shell now opens in Safari with the controls ready for interaction. The next goal is to turn that polished shell into a working tune-up companion.

A generic checklist makes the core flow testable before you introduce model-specific guidance. It also exposes an important limitation because one set of TV menu paths cannot safely fit every Frame generation.

In this step, get ready to:
  • Render a generic checklist with interactive progress tracking.
  • Calculate faster, unchanged, or slower measurement results.
  • Switch model years to reveal the generic guidance mismatch.
Build the generic checklist

The empty taskList is the display area for your first tune-up tasks. JavaScript state keeps the selected year, measurements, and completed task IDs available while the page is open.

  • Switch back to the Claude Code session running inside $HOME/Documents/frame-refresh.
  • Ask Claude to edit only app.js for the first generic flow. Require in-memory state for modelYear, beforeTime, afterTime, and completed task IDs. Require three generic checklist cards, checkbox listeners, model selection tracking, a synchronized yearBadge, and live updates through the existing progressBar and progressText IDs.
  • Switch back to Safari after Claude finishes editing app.js.
  • Reload the local Frame Refresh page using the circular arrow in Safari's toolbar.

You should see three generic cards titled Cold boot the TV, Check for a software update, and Remove an unused downloaded app. The progress display should begin at 0 of 3.

  • Select the checkbox beside Cold boot the TV.
  • Select the checkbox beside Check for a software update.

You should see both cards marked as complete. The progress display should now read 2 of 3.

Checklist Still Empty?

  • Ask Claude to confirm that app.js renders checklist card markup inside taskList after the page loads.
  • Ask Claude to compare every selector in app.js with the matching IDs in index.html.

Help me debug the empty Frame Refresh checklist.

Calculate the measurement result

A before-and-after comparison is meaningful only when both entries are positive numbers. Once both values pass that check, the result can report a faster time, an unchanged time, or a slower time.

  • Switch back to the Claude Code session from earlier.
  • Ask Claude to extend app.js so report appears only when beforeTime and afterTime contain positive numbers. Require faster, unchanged, and slower branches that update measurementStatus, reportLead, the model year, both measurements, and the completed-task count.
  • Switch back to Safari after Claude finishes the change.
  • Reload the local page using Safari's toolbar.

Before you enter the pair, which result do you expect from a before time of 6.2 seconds and an after time of 3.8 seconds?

  • Enter 6.2 in the Before tune-up field.
  • Enter 3.8 in the After tune-up field.

You should see Measured faster beside the measurement heading. The report should say that Home loaded 2.4 seconds faster with a measured improvement of 39 percent.

  • Replace the value in the After tune-up field with 6.2.

You should see No measured change. The report should recommend repeating the test under similar conditions.

  • Replace the value in the After tune-up field with 7.0.

You should see Retest recommended. The report should say that Home loaded 0.8 seconds slower.

  • Clear the After tune-up field.

The report should hide because both positive measurements are no longer available. The status should return to Waiting for both values.

  • Re-enter 3.8 in the After tune-up field.

That completes the measurement loop. Your companion now turns two observations into a result you can compare.

Report Staying Hidden?

  • Confirm both measurement fields contain numbers greater than 0.
  • Ask Claude to confirm the measurement listeners read values from beforeTime and afterTime whenever either field changes.
  • Ask Claude to confirm that valid measurements remove the hidden state from report.

Help me find why the Frame Refresh report stays hidden.

Expose the model-year mismatch

The interaction works with the current generic task set. The final check asks whether those same instructions remain suitable when the selected Frame generation changes.

Before you change the selector, do you think the task titles and menu paths will change for the other model years?

  • Note the current task titles while 2019-2021 is selected.
  • Select 2018 from the Frame model year selector.

You should see the year badge change to 2018 guidance. The same generic task titles and paths remain on screen.

  • Select 2022 from the Frame model year selector.

You should see the year badge change again while the checklist remains generic. This shortfall is deliberate because it proves that the working interaction still lacks safe model-aware guidance.

✔️ Awesome, I've got everything!

The first flow works. You can complete tasks, watch progress change, compare measurements, and reproduce the model-year mismatch.

ⓧ I'd like to double check the full code

Use Claude Code to inspect the current implementation without adding year-specific task data yet.

  • Ask Claude to confirm that app.js defines state for modelYear, beforeTime, afterTime, and completed task IDs.
  • Ask Claude to confirm that generic interactive checklist cards render inside taskList.
  • Ask Claude to confirm that checkbox changes update progressBar and progressText.
  • Ask Claude to confirm that measurement logic accepts only positive values and produces faster, unchanged, or slower feedback.
  • Confirm that index.html still contains the matching model, measurement, progress, checklist, report, dialog, and action-button IDs.
  • Confirm that styles.css still provides responsive cards, focus states, dark appearance support, reduced-motion support, and print rules.

The generic flow is alive, and the selector test has revealed the safety gap. Next, you will replace the shared guidance with verified branches for each Frame model-year group.

Add Model-Aware Samsung Guidance

Your measurement flow works. Your model-year test exposed a safety gap in the generic checklist.

Samsung maintenance routes vary across Frame generations. This step replaces the generic guidance with verified branches for each supported year group.

In this step, get ready to:
  • Replace the generic checklist with shared tasks plus model-specific branches.
  • Complete the model-aware Samsung defaults panel with destructive-action safeguards.
  • Verify the checklist plus factory reset route for every supported model-year group.
Replace the generic task data

A model-aware checklist separates universal maintenance from routes that change by year. The selected model controls which extra tasks appear.

What belongs in each branch?

  • Every model includes a cold boot task.
  • Every model includes a software update check.
  • Every model includes unused downloaded-app cleanup.
  • The 2018 branch adds an optional Smart Hub reset.
  • The 2019-2021 branch adds Device Care.
  • The 2019-2021 branch keeps Smart Hub reset as a last resort.
  • The 2022 branch adds Device Care.
  • The 2022 branch adds Home app reordering.
  • The 2022 branch keeps Smart Hub reset as a last resort.
  • Switch back to the Claude Code session running inside your Frame Refresh folder.
  • Ask Claude to inspect the current app.js before making changes.
  • Ask Claude to replace the generic task data with sharedTasks plus modelTasks matching the branch map above. Require getTasks() to combine the shared tasks with the selected model entry. Require renderTasks() to remove completed IDs that do not belong to the selected branch. Tell Claude to preserve the existing measurement logic.
  • Ask Claude to keep an official Samsung source link on every task.
  • Switch back to Safari.
  • Refresh the Frame Refresh page.
  • Select 2022 from the model-year selector.

You'll see six checklist tasks for the 2022 branch. The year badge reads 2022 guidance.

Device Care now appears as a model task. Home app reordering also appears as a model task.

Smart Hub reset appears as a cautioned last-resort task. The visible change proves that the model selector now controls the checklist.

Checklist still looks generic?

  • Confirm that getTasks() returns the shared tasks plus the selected entry from modelTasks.
  • Confirm that the model-year change handler calls renderTasks().
  • Confirm that yearBadge receives the selected model-year text.

Ask for targeted help with my model-year selector changes the badge but does not update the checklist.

Complete the Samsung defaults panel

Full factory reset affects the whole television. The reset sheet keeps this recovery route available without turning it into an accidental browser action.

This part can feel high stakes. The browser only displays instructions while the TV requires the final confirmation.

What must the reset sheet protect?

  • The reset sheet must state that a full factory reset is destructive.
  • The reset sheet must state that the action cannot be undone.
  • The reset sheet must explain that the app only displays the route.
  • The reset sheet must direct the learner to confirm the reset on the TV.
  • The instructions must tell the learner to close any running streaming app if Reset is unavailable.
  • The instructions must offer TV Plus or an HDMI source as alternatives before trying Reset again.
  • The instructions must remind the learner to record app login details.
  • The instructions must remind the learner to record Samsung Account details.
  • The instructions must remind the learner to record Wi-Fi information.
  • The instructions must explain that the default PIN is 0000 unless it was changed.
  • Return to the Claude Code session from earlier.
  • Ask Claude to make one coordinated revision across index.html, styles.css, plus app.js. Tell it to complete the existing resetGuide dialog using the safeguards above. Require the existing resetYearLabel, factoryResetPath, factoryResetFallback, plus factoryResetSource elements to show the selected model's information.
  • Require Claude to define factoryResetGuides for all three model groups. Tell it to connect tvResetButton through openResetGuide(). Tell it to connect resetCloseButton plus resetBackdrop through closeResetGuide().
  • Ask Claude to keep Smart Hub reset inside the model checklist as a separate last-resort task.
  • Ask Claude to preserve the existing responsive styles.
  • Ask Claude to preserve the existing keyboard focus styles.
  • Ask Claude to preserve the existing dark appearance rules.
  • Ask Claude to preserve the existing reduced-motion rules.

Why keep the resets separate?

Smart Hub reset targets downloaded apps plus account state stored in Smart Hub. Full factory reset restores the television's original settings.

The separate presentation makes the level of impact visible before the learner touches the TV. Recovery stays deliberate.

  • Switch back to Safari.
  • Refresh the Frame Refresh page.
  • Click Samsung defaults in the sticky header.

You'll see a side panel headed Restore Samsung defaults. The panel warns that full factory reset is destructive.

For 2022, the panel shows Home > Menu > Settings > All Settings > General & Privacy > Reset.

The panel explains that the browser only displays the route. It also directs you to complete the reset on the TV.

  • Click Close in the panel header.

The reset sheet closes. Your selected model-year guidance remains visible underneath.

Samsung defaults panel does not open?

  • Confirm that tvResetButton matches the button ID in index.html.
  • Confirm that resetGuide matches the dialog ID in index.html.
  • Confirm that the click listener calls openResetGuide().

Ask for help with my Samsung defaults button does not open resetGuide.

Verify every model-year branch

Each branch now carries different maintenance routes plus a different factory reset guide. Testing all three prevents one model's instructions from leaking into another model's checklist.

Before you run this check, do you expect the checklist plus Samsung defaults route to change every time you select a different year group?

  • Select 2018 from the model-year selector.
  • Click Samsung defaults.

You'll see the shared maintenance tasks plus the optional Smart Hub reset route Settings > Support > Self Diagnosis > Reset Smart Hub.

The factory reset panel shows Home > Settings > Support > Self Diagnosis > Reset for the older-series route.

  • Click the shaded backdrop outside the reset panel.

The panel closes through resetBackdrop. The 2018 guidance badge remains selected.

  • Select 2019-2021 from the model-year selector.
  • Click Samsung defaults.

You'll see Device Care plus the Smart Hub reset route Settings > Support > Device Care > Self Diagnosis > Reset Smart Hub.

The factory reset guidance includes Settings > General > Reset. The fallback includes Settings > Support > Self Diagnosis > Reset.

  • Click Close in the panel header.
  • Select 2022 from the model-year selector.
  • Click Samsung defaults.

You'll see Device Care plus Home app reordering. The Smart Hub route reads Home > Menu > Settings > Support > Device Care > Self Diagnosis > Reset Smart Hub.

The factory reset route reads Home > Menu > Settings > All Settings > General & Privacy > Reset. The destructive warning remains visible for every model year.

The checklist now changes with each selected year. The reset sheet also changes its path plus official source link.

✔️ The guidance changes by year

Your checklist plus Samsung defaults panel now follow the selected Frame generation.

ⓧ I'd like to double check the full code

The JavaScript now defines sharedTasks for cold boot. It also covers software update plus unused downloaded-app cleanup.

The JavaScript now defines modelTasks plus factoryResetGuides for 2018, 2019-2021, plus 2022.

The JavaScript renders model-specific Samsung routes plus official source links. It treats Smart Hub reset plus full factory reset as separate cautioned actions.

The HTML includes the complete resetGuide dialog. It contains resetYearLabel, factoryResetPath, factoryResetFallback, factoryResetSource, resetBackdrop, plus resetCloseButton.

The HTML contains the destructive full-factory-reset warning. It explains that the learner must confirm the reset on the TV.

The stylesheet includes complete task-card plus caution-card styling. It also includes the reset sheet's backdrop plus panel styling.

The stylesheet preserves keyboard focus plus responsive behavior. It also preserves dark appearance plus reduced-motion rules.

The stylesheet preserves the existing print rules.

One model still shows the wrong route?

  • Confirm that each key in modelTasks matches an option value in modelYear.
  • Confirm that each key in factoryResetGuides matches the same model-year value.
  • Confirm that the model-year change handler calls renderResetGuide().

Ask for targeted help with one Frame model-year branch shows another model's Samsung routes.

That closes the safety gap you uncovered. Every supported Frame generation now receives its own conservative guidance.

Next, you'll preserve progress across refreshes plus turn the measurements into a report you can print.

Save Progress and Print the Report

Your model-aware checklist now gives each Samsung The Frame generation the correct maintenance guidance. A page reload can still erase the tune-up record before you finish.

This step uses Web Storage to preserve progress when browser storage is available. You will direct Claude Code to calculate the final result before preparing a focused report for Safari.

In this step, get ready to:
  • Restore the selected model year, measurements, and completed tasks after a reload.
  • Calculate faster, unchanged, or slower results from positive measurements.
  • Verify the app reset, Samsung defaults panel, and printable report.
Save progress across reloads

The app needs one saved state record for the selected year, both measurements, and the completed task IDs. The key STORAGE_KEY keeps that record under the stable name frameRefreshStateV1.

Why handle storage errors?

Browser storage can be unavailable or blocked. Error handling lets the current session continue even when localStorage cannot load or save progress.

  • Switch back to the Claude Code session for $HOME/Documents/frame-refresh.
  • Ask Claude to compare app.js with the Complete Code for this step.
  • Ask Claude to add STORAGE_KEY, defaultState, loadState(), saveState(), and hydrateInterface().
  • Ask Claude to wrap each localStorage operation in error handling.
  • Ask Claude to preserve the existing model-aware task data and Samsung defaults behavior.
  • Switch back to the Frame Refresh page in Safari.
  • Select 2022 in the Frame model year selector.

You should see the 2022 guidance badge. The checklist should include the Home reordering task.

  • Select the checkbox for Cold boot the TV.
  • Enter 6.2 in the Before tune-up field.

You should see the progress count increase. The save status should read Progress saved in this browser.

Before you reload, predict whether the selected year, measurement, and completed task will return.

  • Reload the Frame Refresh page using Safari's toolbar control.

You should see 2022 selected again. The completed task and 6.2 measurement should also return.

That persistence loop is working. Frame Refresh can now survive a reload without losing the current tune-up record.

Progress missing after a reload?

  • Confirm that loadState() runs before the interface is hydrated.
  • Confirm that each model, measurement, or checklist change calls saveState().
  • Check that saveStatus shows a storage message after the page loads.

Ask for help tracing the saved state.

Finish the report and safety controls

The report appears only when both measurements are positive numbers. It compares those values to label the result as faster, unchanged, or slower.

The app reset clears only the browser record. The Samsung defaults control continues to display on-device instructions without sending a reset command to the TV.

  • Switch back to the Claude Code session from earlier.
  • Ask Claude to complete app.js using the final JavaScript from the Complete Code.
  • Include updateProgress(), readPositiveNumber(), updateReport(), and handleMeasurement() in that revision request.
  • Include the final listeners for printing, app progress reset, panel closure, and focus restoration in the same request.
  • Ask Claude to preserve all existing Samsung guidance without adding external packages.
  • Switch back to Frame Refresh in Safari.
  • Enter 6.2 in the Before tune-up field.
  • Enter 3.8 in the After tune-up field.

You should see the Frame Refresh report. Its lead should say that Home loaded 2.4 seconds faster with a measured improvement of 39 percent.

  • Replace the after measurement with 6.2.

You should see No measured change in the measurement status. The report should recommend another test under similar conditions.

  • Replace the after measurement with 7.0.

You should see Retest recommended in the measurement status. The report should say that Home loaded 0.8 seconds slower.

  • Restore the after measurement to 3.8.
  • Click Samsung defaults in the sticky header.
  • Press Escape.

You should see the panel close. Keyboard focus should return to the Samsung defaults control.

  • Click Samsung defaults again.
  • Click the shaded backdrop outside the panel.

You should see the panel close through the backdrop. Your selected year and progress should remain unchanged.

Resetting progress can feel risky beside the factory reset guidance. This confirmation removes only the Frame Refresh state stored in Safari.

  • Click Reset app.
  • Dismiss the confirmation.

Your selected year, measurements, and completed tasks should remain in place. The confirmation protects your record from an accidental click.

  • Click Reset app again.
  • Approve the confirmation.

You should see the default 2019-2021 model group with empty measurements. The checklist progress should return to zero.

Report or reset behavior missing?

  • Confirm that both measurements pass through readPositiveNumber() before the report becomes visible.
  • Confirm that resetButton removes only the record named by STORAGE_KEY.
  • Confirm that closeResetGuide() returns focus through previousFocus.

Ask for help comparing the final interactions.

Audit and print the finished app

A final file audit catches mismatched IDs or references that can disconnect correct logic from the interface. It also confirms that storage, appearance preferences, safety controls, and printing work as one system.

  • Switch back to the Claude Code session from earlier.
  • Ask Claude to compare index.html, styles.css, and app.js with the Complete Code.
  • Ask Claude to check every element ID and file reference.
  • Ask Claude to check labels, retained focus, storage errors, responsive layout, system appearance, reduced motion, reset warnings, and print styling.
  • Ask Claude to correct every difference without adding external packages.

Use these tabs to confirm that all three files now match the final Frame Refresh app.

✔️ Awesome, I've got everything!

Your three files now match the finished Frame Refresh app. Save each file before running the final checks.

ⓧ I'd like to double check the full code

Compare your final index.html with this complete reference.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="color-scheme" content="light dark">
  <title>Frame Refresh</title>
  <link rel="stylesheet" href="styles.css">
  <script src="app.js" defer></script>
</head>
<body>
  <header class="app-bar">
    <a class="brand" href="#top" aria-label="Frame Refresh home">
      <span class="brand-mark" aria-hidden="true"></span>
      <span>Frame Refresh</span>
    </a>
    <div class="app-actions no-print">
      <button class="button button-primary" id="tvResetButton" type="button" aria-controls="resetGuide">Samsung defaults</button>
      <button class="button button-quiet" id="resetButton" type="button">Reset app</button>
    </div>
  </header>

  <main class="app-shell" id="top">
    <aside class="sidebar glass-card" aria-labelledby="setupTitle">
      <p class="eyebrow">Your television</p>
      <h2 id="setupTitle">Choose the model year</h2>
      <p class="muted">The menu guidance changes to match Samsung's documented year groups.</p>

      <label for="modelYear">Frame model year</label>
      <select id="modelYear">
        <option value="2018">2018</option>
        <option value="2019-2021">2019-2021</option>
        <option value="2022">2022</option>
      </select>

      <div class="progress-block">
        <div class="progress-copy">
          <span>Checklist progress</span>
          <strong id="progressText">0 of 0</strong>
        </div>
        <progress id="progressBar" max="1" value="0">0 percent</progress>
      </div>

      <p class="save-status" id="saveStatus" role="status" aria-live="polite">Checking browser storage...</p>
    </aside>

    <div class="content-column">
      <section class="hero glass-card">
        <p class="eyebrow">A guided Smart Hub tune-up</p>
        <h1>A calmer path to a faster Frame.</h1>
        <p class="hero-copy">Measure the Home menu, follow conservative Samsung guidance, and keep a report of what changed.</p>
        <div class="notice">
          <strong>Companion, not custom firmware.</strong>
          <span>This page guides the TV's existing controls. It does not modify or remotely reset the Samsung operating system.</span>
        </div>
      </section>

      <section class="glass-card metrics" aria-labelledby="measureTitle">
        <div class="section-heading">
          <div>
            <p class="eyebrow">Before and after</p>
            <h2 id="measureTitle">Measure Home loading time</h2>
          </div>
          <span class="status-pill" id="measurementStatus" role="status" aria-live="polite">Waiting for both values</span>
        </div>

        <p class="muted">From live TV, press Home and count until the app row is ready to use. Repeat the same test after the checklist.</p>

        <div class="field-grid">
          <div>
            <label for="beforeTime">Before tune-up</label>
            <div class="number-field">
              <input id="beforeTime" type="number" min="0.1" step="0.1" inputmode="decimal" placeholder="6.2">
              <span>seconds</span>
            </div>
          </div>
          <div>
            <label for="afterTime">After tune-up</label>
            <div class="number-field">
              <input id="afterTime" type="number" min="0.1" step="0.1" inputmode="decimal" placeholder="3.8">
              <span>seconds</span>
            </div>
          </div>
        </div>
      </section>

      <section class="checklist-section" aria-labelledby="checklistTitle">
        <div class="section-heading">
          <div>
            <p class="eyebrow">Guided actions</p>
            <h2 id="checklistTitle">Tune-up checklist</h2>
          </div>
          <span class="year-badge" id="yearBadge">2019-2021 guidance</span>
        </div>
        <div class="task-list" id="taskList"></div>
      </section>

      <section class="glass-card report" id="report" aria-labelledby="reportTitle" hidden>
        <div class="section-heading">
          <div>
            <p class="eyebrow">Your result</p>
            <h2 id="reportTitle">Frame Refresh report</h2>
          </div>
          <button class="button button-primary no-print" id="printButton" type="button">Print report</button>
        </div>

        <p class="report-lead" id="reportLead"></p>
        <dl class="report-grid">
          <div><dt>Model group</dt><dd id="reportYear"></dd></div>
          <div><dt>Before</dt><dd id="reportBefore"></dd></div>
          <div><dt>After</dt><dd id="reportAfter"></dd></div>
          <div><dt>Completed</dt><dd id="reportCompleted"></dd></div>
        </dl>
        <p class="muted">Measurements are observational and can vary with network state, app updates, and background activity.</p>
      </section>
    </div>
  </main>

  <section class="reset-sheet no-print" id="resetGuide" role="dialog" aria-modal="true" aria-labelledby="resetGuideTitle" hidden>
    <div class="reset-backdrop" id="resetBackdrop"></div>
    <div class="reset-panel glass-card">
      <div class="panel-header">
        <div>
          <p class="eyebrow">Recovery option</p>
          <h2 id="resetGuideTitle">Restore Samsung defaults</h2>
        </div>
        <button class="button button-quiet" id="resetCloseButton" type="button">Close</button>
      </div>

      <div class="danger-notice">
        <strong>Full factory reset is destructive and cannot be undone.</strong>
        <span>The app only shows the route. You must confirm the reset on the TV.</span>
      </div>

      <div class="reset-level">
        <p class="eyebrow" id="resetYearLabel"></p>
        <h3>Full factory reset</h3>
        <p class="reset-path" id="factoryResetPath"></p>
        <p class="muted" id="factoryResetFallback"></p>
        <ol class="reset-checklist">
          <li>Close any running streaming app and switch to TV Plus or an HDMI source if Reset is unavailable.</li>
          <li>Write down app logins, your Samsung Account details, and Wi-Fi network information.</li>
          <li>Enter your TV PIN when prompted. The default is 0000 unless you changed it.</li>
          <li>Confirm Reset, then OK. Complete first-time setup after the TV restarts.</li>
        </ol>
        <a id="factoryResetSource" target="_blank" rel="noopener noreferrer">Open official Samsung factory reset source</a>
      </div>
    </div>
  </section>
</body>
</html>

What to compare in the HTML

Confirm that every visible control has the exact ID referenced by app.js. Check that the report and reset guide begin with the hidden attribute.

Compare your final styles.css with this complete reference.

:root {
  color-scheme: light dark;
  font-family: system-ui, sans-serif;
  --page: #eef3fb;
  --page-deep: #dce8f8;
  --surface: rgba(255, 255, 255, 0.76);
  --surface-solid: #ffffff;
  --text: #172033;
  --muted: #526078;
  --line: rgba(63, 83, 116, 0.18);
  --accent: #1769e0;
  --accent-strong: #0d55be;
  --accent-soft: #e5f0ff;
  --success: #176b48;
  --warning: #9a4d00;
  --warning-soft: #fff2df;
  --danger: #a82727;
  --danger-soft: #fff0ee;
  --shadow: 0 24px 70px rgba(47, 72, 111, 0.16);
  --radius-large: 28px;
  --radius-medium: 18px;
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  color: var(--text);
  background:
    radial-gradient(circle at 10% 5%, rgba(105, 167, 255, 0.38), transparent 34rem),
    radial-gradient(circle at 90% 18%, rgba(186, 149, 255, 0.22), transparent 30rem),
    linear-gradient(145deg, var(--page), var(--page-deep));
}
body.has-dialog { overflow: hidden; }
button, input, select { font: inherit; }
button, select, input[type="checkbox"] { cursor: pointer; }
button:focus-visible, input:focus-visible, select:focus-visible, a:focus-visible {
  outline: 3px solid rgba(23, 105, 224, 0.45);
  outline-offset: 3px;
}

.app-bar {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  min-height: 72px;
  padding: 14px clamp(20px, 5vw, 72px);
  border-bottom: 1px solid var(--line);
  background: rgba(250, 252, 255, 0.72);
  -webkit-backdrop-filter: blur(24px) saturate(130%);
  backdrop-filter: blur(24px) saturate(130%);
}
.brand {
  display: inline-flex;
  gap: 10px;
  align-items: center;
  color: var(--text);
  font-weight: 720;
  text-decoration: none;
  letter-spacing: -0.02em;
}
.brand-mark {
  width: 18px;
  height: 18px;
  border-radius: 6px;
  background: linear-gradient(145deg, #66b4ff, #165cd7);
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.75);
}
.app-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
.app-shell {
  display: grid;
  grid-template-columns: minmax(240px, 310px) minmax(0, 820px);
  gap: 28px;
  width: min(1180px, calc(100% - 40px));
  margin: 36px auto 80px;
  align-items: start;
}
.content-column { display: grid; gap: 24px; }
.glass-card {
  border: 1px solid rgba(255, 255, 255, 0.72);
  border-radius: var(--radius-large);
  background: var(--surface);
  box-shadow: var(--shadow);
  -webkit-backdrop-filter: blur(28px) saturate(125%);
  backdrop-filter: blur(28px) saturate(125%);
}
.sidebar { position: sticky; top: 104px; padding: 26px; }
.hero, .metrics, .report { padding: clamp(24px, 5vw, 46px); }
.hero h1 {
  max-width: 680px;
  margin: 8px 0 14px;
  font-size: clamp(2.5rem, 7vw, 4.9rem);
  line-height: 0.98;
  letter-spacing: -0.065em;
}
.hero-copy {
  max-width: 660px;
  margin: 0;
  color: var(--muted);
  font-size: clamp(1.05rem, 2.5vw, 1.28rem);
  line-height: 1.6;
}
.eyebrow {
  margin: 0 0 6px;
  color: var(--accent);
  font-size: 0.78rem;
  font-weight: 760;
  letter-spacing: 0.11em;
  text-transform: uppercase;
}
h2 { margin: 0; font-size: clamp(1.35rem, 3vw, 2rem); letter-spacing: -0.035em; }
.muted { color: var(--muted); line-height: 1.55; }
.notice, .danger-notice {
  display: grid;
  gap: 4px;
  margin-top: 28px;
  padding: 16px 18px;
  border-radius: var(--radius-medium);
  line-height: 1.45;
}
.notice { border: 1px solid rgba(23, 105, 224, 0.2); background: var(--accent-soft); }
.notice span, .danger-notice span { color: var(--muted); }
.danger-notice { border: 1px solid rgba(168, 39, 39, 0.28); background: var(--danger-soft); }
label { display: block; margin: 22px 0 8px; font-size: 0.9rem; font-weight: 700; }
select, input[type="number"] {
  width: 100%;
  border: 1px solid var(--line);
  border-radius: 14px;
  color: var(--text);
  background: var(--surface-solid);
}
select { min-height: 48px; padding: 0 14px; }
.section-heading, .progress-copy, .panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
}
.progress-block { margin-top: 28px; }
.progress-copy { margin-bottom: 10px; color: var(--muted); font-size: 0.88rem; }
.progress-copy strong { color: var(--text); }
progress {
  width: 100%;
  height: 9px;
  overflow: hidden;
  border: 0;
  border-radius: 999px;
  background: rgba(85, 105, 137, 0.16);
}
progress::-webkit-progress-bar { background: rgba(85, 105, 137, 0.16); }
progress::-webkit-progress-value, progress::-moz-progress-bar {
  border-radius: 999px;
  background: linear-gradient(90deg, #4aa5ff, var(--accent));
}
.save-status { min-height: 1.3em; margin: 16px 0 0; color: var(--muted); font-size: 0.82rem; }
.status-pill, .year-badge {
  flex: 0 0 auto;
  padding: 8px 11px;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--muted);
  background: var(--surface-solid);
  font-size: 0.78rem;
  font-weight: 700;
}
.field-grid, .report-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.number-field {
  display: flex;
  align-items: center;
  border: 1px solid var(--line);
  border-radius: 15px;
  background: var(--surface-solid);
}
.number-field:focus-within { outline: 3px solid rgba(23, 105, 224, 0.22); border-color: var(--accent); }
.number-field input { min-width: 0; min-height: 54px; padding: 0 6px 0 15px; border: 0; outline: 0; background: transparent; }
.number-field span { padding-right: 15px; color: var(--muted); font-size: 0.86rem; }
.checklist-section { padding: 12px 2px 0; }
.task-list { display: grid; gap: 14px; margin-top: 18px; }
.task-card {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 16px;
  padding: 22px;
  border: 1px solid var(--line);
  border-radius: var(--radius-medium);
  background: var(--surface);
  box-shadow: 0 12px 32px rgba(48, 71, 104, 0.09);
}
.task-card.is-complete { border-color: rgba(23, 107, 72, 0.38); }
.task-card.is-caution { border-color: rgba(154, 77, 0, 0.28); background: linear-gradient(145deg, var(--surface), var(--warning-soft)); }
.task-card input { width: 22px; height: 22px; margin: 2px 0 0; accent-color: var(--accent); }
.task-card h3, .task-card h3 label { margin: 0; font-size: 1.05rem; }
.task-card p { margin: 7px 0 0; color: var(--muted); line-height: 1.55; }
.task-card .path { color: var(--text); }
.task-card a, .reset-level a { display: inline-block; margin-top: 10px; color: var(--accent-strong); font-size: 0.86rem; font-weight: 700; }
.report-lead { margin: 22px 0; font-size: clamp(1.3rem, 4vw, 2rem); font-weight: 760; letter-spacing: -0.035em; }
.report-grid { margin: 0; }
.report-grid div { padding: 16px; border: 1px solid var(--line); border-radius: 15px; background: var(--surface-solid); }
.report-grid dt { color: var(--muted); font-size: 0.78rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; }
.report-grid dd { margin: 6px 0 0; font-size: 1.15rem; font-weight: 750; }
.button { min-height: 42px; padding: 0 16px; border: 1px solid transparent; border-radius: 999px; font-weight: 720; }
.button-primary { color: #ffffff; background: var(--accent); box-shadow: 0 8px 20px rgba(23, 105, 224, 0.24); }
.button-primary:hover { background: var(--accent-strong); }
.button-quiet { border-color: var(--line); color: var(--text); background: var(--surface); }
.reset-sheet { position: fixed; inset: 0; z-index: 30; display: grid; justify-items: end; }
.reset-backdrop { position: absolute; inset: 0; background: rgba(4, 8, 18, 0.52); }
.reset-panel {
  position: relative;
  z-index: 1;
  width: min(560px, 100%);
  height: 100%;
  overflow: auto;
  padding: clamp(24px, 5vw, 42px);
  border-radius: 28px 0 0 28px;
}
.reset-level { margin-top: 22px; padding: 22px; border: 1px solid var(--line); border-radius: var(--radius-medium); background: var(--surface-solid); }
.reset-level h3 { margin: 0; font-size: 1.25rem; }
.reset-path { padding: 14px; border-radius: 13px; color: var(--text); background: var(--accent-soft); font-weight: 720; line-height: 1.55; }
.reset-checklist { padding-left: 22px; color: var(--muted); line-height: 1.6; }
.reset-checklist li + li { margin-top: 8px; }
[hidden] { display: none !important; }

@media (max-width: 850px) {
  .app-shell { grid-template-columns: 1fr; }
  .sidebar { position: static; }
}
@media (max-width: 620px) {
  .app-bar { align-items: flex-start; padding-inline: 16px; }
  .app-actions { display: grid; }
  .app-shell { width: min(100% - 24px, 1180px); margin-top: 18px; }
  .section-heading, .panel-header { align-items: start; }
  .field-grid, .report-grid { grid-template-columns: 1fr; }
  .status-pill, .year-badge { justify-self: start; }
  .reset-panel { border-radius: 0; }
}
@media (prefers-color-scheme: dark) {
  :root {
    --page: #0b1020;
    --page-deep: #111a2f;
    --surface: rgba(24, 32, 52, 0.76);
    --surface-solid: #1a243a;
    --text: #f4f7ff;
    --muted: #b4bfd2;
    --line: rgba(204, 220, 255, 0.17);
    --accent: #6aafff;
    --accent-strong: #9bcbff;
    --accent-soft: rgba(60, 125, 220, 0.18);
    --warning: #ffc47d;
    --warning-soft: rgba(154, 77, 0, 0.16);
    --danger: #ffaaa3;
    --danger-soft: rgba(168, 39, 39, 0.18);
    --shadow: 0 26px 80px rgba(0, 0, 0, 0.34);
  }
  .app-bar { background: rgba(11, 16, 32, 0.72); }
  .button-primary { color: #07101e; }
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}
@media print {
  :root { color-scheme: light; --text: #111111; --muted: #444444; --surface: #ffffff; --surface-solid: #ffffff; --line: #cccccc; }
  body { background: #ffffff; }
  .app-bar, .sidebar, .hero, .metrics, .checklist-section, .no-print { display: none !important; }
  .app-shell, .content-column { display: block; width: 100%; margin: 0; }
  .report { display: block !important; border: 0; box-shadow: none; padding: 0; }
}

What to compare in the CSS

Confirm that keyboard focus remains visible. Check that the responsive, dark appearance, reduced-motion, and print rules are present.

Compare your final app.js with this complete reference.

const STORAGE_KEY = "frameRefreshStateV1";

const defaultState = {
  modelYear: "2019-2021",
  beforeTime: "",
  afterTime: "",
  completed: []
};

const sharedTasks = [
  {
    id: "cold-boot",
    title: "Cold boot the TV",
    path: "With the TV on, hold the remote Power button until the TV turns off and on again. If the remote is unavailable, unplug the TV for 30 seconds, reconnect it, and turn it on.",
    detail: "This is different from standby and clears the TV's memory cache.",
    source: "https://www.samsung.com/us/support/troubleshoot/TSG10002361/"
  },
  {
    id: "software-update",
    title: "Check for a software update",
    path: "Settings > Support > Software Update > Update Now",
    detail: "Do not turn off the TV during installation. Video and audio settings may return to defaults after an update.",
    source: "https://www.samsung.com/us/support/troubleshoot/TSG10000062/"
  },
  {
    id: "app-cleanup",
    title: "Remove an unused downloaded app",
    path: "Apps > Settings > select the downloaded app > Delete",
    detail: "Deletion removes that app's data. If Delete is unavailable, leave the preinstalled or recommended app in place.",
    source: "https://www.samsung.com/us/support/troubleshoot/TSG10000062/"
  }
];

const modelTasks = {
  "2018": [
    {
      id: "smart-hub-reset",
      title: "Optional last resort: Reset Smart Hub",
      path: "Settings > Support > Self Diagnosis > Reset Smart Hub",
      detail: "Caution: gather every app login first. This signs you out and resets Smart Hub settings. The default PIN is 0000 unless you changed it.",
      source: "https://www.samsung.com/us/support/troubleshoot/TSG10000062/",
      caution: true
    }
  ],
  "2019-2021": [
    {
      id: "device-care",
      title: "Run Device Care if it appears",
      path: "Settings > Support > Device Care > Start Device Care",
      detail: "Device Care diagnoses the TV, clears cached memory, and closes unused background apps. If the option is absent, skip this task.",
      source: "https://www.samsung.com/ie/support/tv-audio-video/how-to-use-the-device-care-on-my-samsung-smart-tv/"
    },
    {
      id: "smart-hub-reset",
      title: "Optional last resort: Reset Smart Hub",
      path: "Settings > Support > Device Care > Self Diagnosis > Reset Smart Hub",
      detail: "Caution: gather every app login first. This signs you out and resets Smart Hub settings. The default PIN is 0000 unless you changed it.",
      source: "https://www.samsung.com/us/support/troubleshoot/TSG10000062/",
      caution: true
    }
  ],
  "2022": [
    {
      id: "device-care",
      title: "Run Device Care",
      path: "Settings > Support > Device Care > Start Device Care",
      detail: "The scan diagnoses the TV, clears cached memory, and closes unused background apps.",
      source: "https://www.samsung.com/ie/support/tv-audio-video/how-to-use-the-device-care-on-my-samsung-smart-tv/"
    },
    {
      id: "home-reorder",
      title: "Put frequent apps first",
      path: "Apps > List Edit > select an app > Move > use Left or Right > Select",
      detail: "Removing an app from the Home Screen is different from deleting it and does not erase the installed app.",
      source: "https://www.samsung.com/sg/support/tv-audio-video/2022-how-to-install-apps-and-manage-downloaded-apps-on-your-samsung-tv/"
    },
    {
      id: "smart-hub-reset",
      title: "Optional last resort: Reset Smart Hub",
      path: "Home > Menu > Settings > Support > Device Care > Self Diagnosis > Reset Smart Hub",
      detail: "Caution: gather every app login first. This resets Smart Hub settings, downloaded apps, and stored accounts. The default PIN is 0000 unless you changed it.",
      source: "https://www.samsung.com/sg/support/tv-audio-video/2022-how-to-install-apps-and-manage-downloaded-apps-on-your-samsung-tv/",
      caution: true
    }
  ]
};

const factoryResetGuides = {
  "2018": {
    path: "Home > Settings > Support > Self Diagnosis > Reset",
    fallback: "This is Samsung's older-series The Frame route.",
    source: "https://www.samsung.com/in/support/tv-audio-video/how-to-restore-the-frame-tv-to-factory-setting/"
  },
  "2019-2021": {
    path: "Home > Settings > General > Reset",
    fallback: "If Reset is not under General, use Settings > Support > Self Diagnosis > Reset or consult the model manual.",
    source: "https://www.samsung.com/au/support/tv-audio-video/how-to-reset-tv/"
  },
  "2022": {
    path: "Home > Menu > Settings > All Settings > General & Privacy > Reset",
    fallback: "Menu wording can vary by region and software version.",
    source: "https://www.samsung.com/au/support/tv-audio-video/how-to-reset-tv/"
  }
};

const elements = {
  modelYear: document.querySelector("#modelYear"),
  beforeTime: document.querySelector("#beforeTime"),
  afterTime: document.querySelector("#afterTime"),
  taskList: document.querySelector("#taskList"),
  progressBar: document.querySelector("#progressBar"),
  progressText: document.querySelector("#progressText"),
  saveStatus: document.querySelector("#saveStatus"),
  yearBadge: document.querySelector("#yearBadge"),
  measurementStatus: document.querySelector("#measurementStatus"),
  report: document.querySelector("#report"),
  reportLead: document.querySelector("#reportLead"),
  reportYear: document.querySelector("#reportYear"),
  reportBefore: document.querySelector("#reportBefore"),
  reportAfter: document.querySelector("#reportAfter"),
  reportCompleted: document.querySelector("#reportCompleted"),
  printButton: document.querySelector("#printButton"),
  resetButton: document.querySelector("#resetButton"),
  tvResetButton: document.querySelector("#tvResetButton"),
  resetGuide: document.querySelector("#resetGuide"),
  resetBackdrop: document.querySelector("#resetBackdrop"),
  resetCloseButton: document.querySelector("#resetCloseButton"),
  resetYearLabel: document.querySelector("#resetYearLabel"),
  factoryResetPath: document.querySelector("#factoryResetPath"),
  factoryResetFallback: document.querySelector("#factoryResetFallback"),
  factoryResetSource: document.querySelector("#factoryResetSource")
};

let state = loadState();
let previousFocus = null;

function getTasks() {
  return [...sharedTasks, ...modelTasks[state.modelYear]];
}

function loadState() {
  try {
    const saved = window.localStorage.getItem(STORAGE_KEY);
    if (!saved) return { ...defaultState };
    const parsed = JSON.parse(saved);
    return {
      modelYear: modelTasks[parsed.modelYear] ? parsed.modelYear : defaultState.modelYear,
      beforeTime: String(parsed.beforeTime || ""),
      afterTime: String(parsed.afterTime || ""),
      completed: Array.isArray(parsed.completed) ? parsed.completed : []
    };
  } catch (error) {
    return { ...defaultState };
  }
}

function saveState() {
  try {
    window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
    elements.saveStatus.textContent = "Progress saved in this browser.";
  } catch (error) {
    elements.saveStatus.textContent = "Browser storage is unavailable. This session still works.";
  }
}

function renderResetGuide() {
  const guide = factoryResetGuides[state.modelYear];
  elements.resetYearLabel.textContent = `${state.modelYear} guidance`;
  elements.factoryResetPath.textContent = guide.path;
  elements.factoryResetFallback.textContent = guide.fallback;
  elements.factoryResetSource.href = guide.source;
}

function openResetGuide() {
  previousFocus = document.activeElement;
  renderResetGuide();
  elements.resetGuide.hidden = false;
  document.body.classList.add("has-dialog");
  elements.resetCloseButton.focus();
}

function closeResetGuide() {
  elements.resetGuide.hidden = true;
  document.body.classList.remove("has-dialog");
  if (previousFocus) previousFocus.focus();
}

function renderTasks() {
  const tasks = getTasks();
  const validIds = new Set(tasks.map((task) => task.id));
  state.completed = state.completed.filter((id) => validIds.has(id));

  elements.taskList.innerHTML = tasks.map((task) => {
    const complete = state.completed.includes(task.id);
    const cardClasses = ["task-card"];
    if (complete) cardClasses.push("is-complete");
    if (task.caution) cardClasses.push("is-caution");

    return `
      <article class="${cardClasses.join(" ")}">
        <input id="task-${task.id}" type="checkbox" data-task-id="${task.id}" ${complete ? "checked" : ""}>
        <div>
          <h3><label for="task-${task.id}">${task.title}</label></h3>
          <p class="path"><strong>Path:</strong> ${task.path}</p>
          <p>${task.detail}</p>
          <a href="${task.source}" target="_blank" rel="noopener noreferrer">Open official Samsung source</a>
        </div>
      </article>
    `;
  }).join("");

  document.querySelectorAll("[data-task-id]").forEach((checkbox) => {
    checkbox.addEventListener("change", handleTaskChange);
  });
  updateProgress();
}

function handleTaskChange(event) {
  const taskId = event.currentTarget.dataset.taskId;
  if (event.currentTarget.checked) {
    state.completed = [...new Set([...state.completed, taskId])];
  } else {
    state.completed = state.completed.filter((id) => id !== taskId);
  }
  event.currentTarget.parentElement.classList.toggle("is-complete", event.currentTarget.checked);
  saveState();
  updateProgress();
  updateReport();
}

function updateProgress() {
  const tasks = getTasks();
  const completedCount = tasks.filter((task) => state.completed.includes(task.id)).length;
  elements.progressBar.max = tasks.length;
  elements.progressBar.value = completedCount;
  elements.progressBar.textContent = `${completedCount} of ${tasks.length}`;
  elements.progressText.textContent = `${completedCount} of ${tasks.length}`;
}

function readPositiveNumber(value) {
  const number = Number(value);
  return Number.isFinite(number) && number > 0 ? number : null;
}

function updateReport() {
  const before = readPositiveNumber(state.beforeTime);
  const after = readPositiveNumber(state.afterTime);
  const tasks = getTasks();
  const completedCount = tasks.filter((task) => state.completed.includes(task.id)).length;

  if (before === null || after === null) {
    elements.report.hidden = true;
    elements.measurementStatus.textContent = "Waiting for both values";
    return;
  }

  const delta = before - after;
  let lead;
  if (delta > 0) {
    const percentage = Math.round((delta / before) * 100);
    lead = `Home loaded ${delta.toFixed(1)} seconds faster, a measured improvement of ${percentage} percent.`;
    elements.measurementStatus.textContent = "Measured faster";
  } else if (delta === 0) {
    lead = "The before-and-after measurements are the same. Repeat the test once under similar conditions.";
    elements.measurementStatus.textContent = "No measured change";
  } else {
    lead = `Home loaded ${Math.abs(delta).toFixed(1)} seconds slower. Repeat the test after one more cold boot.`;
    elements.measurementStatus.textContent = "Retest recommended";
  }

  elements.reportLead.textContent = lead;
  elements.reportYear.textContent = state.modelYear;
  elements.reportBefore.textContent = `${before.toFixed(1)} seconds`;
  elements.reportAfter.textContent = `${after.toFixed(1)} seconds`;
  elements.reportCompleted.textContent = `${completedCount} of ${tasks.length} actions`;
  elements.report.hidden = false;
}

function handleMeasurement(event) {
  state[event.currentTarget.id] = event.currentTarget.value;
  saveState();
  updateReport();
}

function hydrateInterface() {
  elements.modelYear.value = state.modelYear;
  elements.beforeTime.value = state.beforeTime;
  elements.afterTime.value = state.afterTime;
  elements.yearBadge.textContent = `${state.modelYear} guidance`;
  renderTasks();
  renderResetGuide();
  updateReport();
  saveState();
}

elements.modelYear.addEventListener("change", (event) => {
  state.modelYear = event.currentTarget.value;
  state.completed = [];
  elements.yearBadge.textContent = `${state.modelYear} guidance`;
  saveState();
  renderTasks();
  renderResetGuide();
  updateReport();
});
elements.beforeTime.addEventListener("input", handleMeasurement);
elements.afterTime.addEventListener("input", handleMeasurement);
elements.printButton.addEventListener("click", () => window.print());
elements.tvResetButton.addEventListener("click", openResetGuide);
elements.resetCloseButton.addEventListener("click", closeResetGuide);
elements.resetBackdrop.addEventListener("click", closeResetGuide);
document.addEventListener("keydown", (event) => {
  if (event.key === "Escape" && !elements.resetGuide.hidden) closeResetGuide();
});

elements.resetButton.addEventListener("click", () => {
  const shouldReset = window.confirm("Reset all app progress and measurements?");
  if (!shouldReset) return;
  state = { ...defaultState };
  try {
    window.localStorage.removeItem(STORAGE_KEY);
  } catch (error) {
    // The in-memory reset still succeeds when browser storage is unavailable.
  }
  hydrateInterface();
});

hydrateInterface();

What to compare in the JavaScript

Confirm that storage loads before hydrateInterface() rebuilds the visible state. Check that each model, measurement, or checklist change triggers the matching save or render function.

Confirm that dialog closure restores previousFocus. Check that app reset removes only the record named by STORAGE_KEY.

  • Switch back to Frame Refresh in Safari.
  • Select 2022 in the Frame model year selector.

You should see the year badge and checklist switch to the 2022 guidance.

  • Enter 6.2 in the Before tune-up field.
  • Enter 3.8 in the After tune-up field.

You should see the report with the 2022 model group and a faster result.

  • Click Samsung defaults.
  • Press Escape.

You should see the panel close. Focus should return to the control that opened it.

Before you print, predict which parts of the interface should remain in the preview.

  • Click Print report in the report card.

You should see Safari's print preview showing the report without the app bar, sidebar, measurement form, checklist, or Samsung defaults panel.

Print preview showing the full app?

  • Confirm that both measurement fields contain values greater than zero.
  • Confirm that the interactive sections use the no-print class or appear in the print hide list.
  • Confirm that the print rule forces .report to display.

Ask for help checking the report preview.

You have completed the finished Frame Refresh companion. It now preserves progress, measures the result, protects recovery guidance, and produces a report you can keep.

Secret mission

Run a Product-Quality Accessibility Audit

Frame Refresh looks polished at normal size. Test whether it remains usable near 200 percent zoom, through keyboard-only operation, and in print preview. Turn one finding into a focused Claude Code revision request.

Clean Up Your Resources

Clean Up Your Resources

Frame Refresh runs entirely on your Mac. Keeping it creates no ongoing hosting or cloud costs.

Decide whether to keep the app available, pause your work, or remove the local resources.

Resources you used:

  • The index.html file inside $HOME/Documents/frame-refresh.
  • The styles.css file inside $HOME/Documents/frame-refresh.
  • The app.js file inside $HOME/Documents/frame-refresh.
  • Frame Refresh progress saved in Safari when browser storage is available.

Keep everything running

No action is needed. Choose this if you want Frame Refresh ready for your next TV tune-up.

  • Leave the frame-refresh folder in $HOME/Documents.
  • Reopen index.html in Safari whenever your TV needs maintenance.

Your saved measurements remain available when browser storage works. Keeping the folder creates no ongoing cost.

Pause - I'll come back to this later

Shut down the active Claude Code session to pause your work. Your local files remain unchanged.

  • End the active Claude Code session by running this command:
/exit

What does this command do?

The /exit command closes the current Claude Code interaction. It returns control to the Terminal shell.

  • Confirm the Claude Code prompt has closed.

That pauses the session cleanly. Your three app files remain in $HOME/Documents/frame-refresh.

Delete - I don't want to use this again

Remove all project resources to start fresh. Keeping the app costs nothing, so choose this only if you no longer want the local utility.

  • Return to Frame Refresh in Safari.
  • Click Reset app in the sticky header.
  • Confirm the browser prompt.

You'll see both measurement fields clear. The checklist progress returns to its starting state.

  • Locate the frame-refresh folder inside $HOME/Documents using Finder.
  • Move the frame-refresh folder to Trash.
  • Check $HOME/Documents again.

The frame-refresh folder should no longer appear there. Safari may retain site data until browser data is cleared.

Nice Work!

Nice Work!

You made it! Frame Refresh now opens in Safari as a guided tune-up companion for Samsung The Frame.

What you learned:

  • Directed Claude Code to build a responsive local utility with no external packages.
  • Saved model selection, measurements, and completed tasks with a graceful storage fallback. Calculated faster, unchanged, or slower results from positive measurements. Produced a print-focused report.
  • Replaced generic instructions with model-aware guidance. Kept full factory reset on the TV. Added destructive-action safeguards backed by official Samsung source links.
  • Completed the optional Secret Mission through a product-quality accessibility audit. Tested enlarged zoom, keyboard navigation, focus restoration, and print output.

Ready to quiz yourself?