Lead Tracker started as a demonstration of a small freelance-outreach tool: a queue for today's next actions, an eight-stage board, and enough detail to remember the last conversation.

It ships with 25 fictional seed leads. Demo and real data use separate localStorage keys. The default application needs no application server; the optional Google Sheets connection changes that picture by introducing an Apps Script endpoint.

That distinction is more useful than calling the whole system “backend-free.” There is a browser-only path, and there is a remote handoff a user can choose to configure.

01 / FIELD NOTESThe workflow comes before the storage choice

The Today queue narrows attention to follow-ups and next actions. The board gives the same work a longer view. The source describes lead details with notes, scores and next-action dates, rather than an elaborate team CRM.

The pipeline's eight stages are expressed as a union type:

src/types.ts · source union / TypeScript
status: 'New' | 'Contacted' | 'Followed up' | 'Replied' | 'Call booked' | 'Quoted' | 'Won' | 'Lost';
Source excerpt · not executed in this design sample · scroll code horizontally on narrow screens

This defines the accepted values in the shown type. It does not, by itself, prove that every board column, filter or transition is generated from that declaration. Adding a stage still needs a check of the code that consumes it.

02 / FIELD NOTESBrowser persistence is a scope decision

The source stores the working collection in localStorage and keeps demo data under a separate key. That makes it possible to try the seeded board without overwriting the real collection described in the account.

Browser-local data can survive closing and reopening a page. It is not automatically a shared team database, a backup or a cross-device account. Browser policy and private browsing also affect persistence. Those platform details are clarified in the MDN localStorage reference; they are not new tests of Lead Tracker's storage handling.

For this tool, the optional Sheets connection is the way out of the local-only model.

03 / FIELD NOTESWhat the Sheets request actually sends

The application provides an Apps Script for the user to install in a Sheet and deploy as a web app. The resulting URL is configured in the tool. The published client excerpt sends the collection as a saveAll operation:

src/utils/sync.ts · source saveAll request / TypeScript
export async function syncLeadsToSheets(url: string, leads: Lead[]): Promise<boolean> {
  const response = await fetch(url, {
    method: 'POST',
    mode: 'cors',
    headers: {
      'Content-Type': 'text/plain', // Using text/plain to prevent pre-flight CORS blocks on simple Apps Script setups
    },
    body: JSON.stringify({
      action: 'saveAll',
      leads: leads,
    }),
  });

  if (!response.ok) {
    throw new Error(`Failed to save leads to sheet: ${response.statusText}`);
  }

  const result = await response.json();
  return result.status === 'success';
}
Source excerpt · not executed in this design sample · scroll code horizontally on narrow screens

There are two separate checks. A non-successful HTTP response throws an error. After that, the parsed response must contain status: 'success' for the function to return true.

An HTTP response can therefore arrive without the application acknowledging the save. The local explanation below lets you inspect those cases without contacting Google or sending a lead.

FOLLOW THE ACKNOWLEDGEMENT

A response is not always a saved result.

Inspect the client checks without connecting a Sheet or sending data.

THE BROWSER COLLECTION

Local records.

3 fictional examples
Unchanged by this inspector

DEMO KEY ≠ REAL-DATA KEY
OPTIONAL REMOTE HANDOFF

Apps Script.

Receives a saveAll request
when configured in the app

NOT CONNECTED
ILLUSTRATED PAYLOAD
{ action: 'saveAll',
  leads: [/* full example collection */] }
WHAT THIS RESPONSE MEANS

No remote save attempted.

The local-only path does not need an Apps Script endpoint. This inspector never contacts one.

Read the explanation without interacting

The source client POSTs the complete lead collection with action saveAll. An unsuccessful HTTP response throws. After a successful HTTP response, the JSON status must equal success for the function to return true. A different status is not an acknowledgement. The excerpt does not establish concurrent editing or a remote-to-local merge.

Local response illustration. No network request or spreadsheet change occurs. A success-shaped reply is not proof of two-way sync, permissions or conflict handling.

This is an outbound save operation. The excerpt does not show importing remote changes, resolving simultaneous edits, validating the endpoint's permissions, or recovering from an interrupted response. Calling it a complete synchronization protocol would go beyond the code shown.

04 / FIELD NOTESThe content type is only one part of the request

The body contains JSON, but the declared content type is text/plain. The comment explains the intent: avoid a preflight in a simple Apps Script setup.

text/plain is one of the CORS-safelisted content types when the other simple-request conditions are met. It does not bypass the response's CORS requirements or establish that a particular deployment is correctly configured. See MDN's CORS guide.

Google also documents that Content Service responses redirect to a one-time script.googleusercontent.com URL. The actual deployment and client still need to be tested together; the Content Service documentation describes that behavior. These are technical clarifications, not a claim that this preview connected to the original endpoint.

05 / FIELD NOTESThe next work is about keeping the tool manageable

The original account describes saveAll as rewriting the sheet on each sync. A change-based approach is a proposed improvement, not something the current excerpt implements.

The other two follow-ups are straightforward: add JSON export/import so the collection has another exit, and separate the main component into smaller board and queue pieces.

I would keep those tasks attached to the problem the tool solves. A useful next-action list does not need every CRM feature. It does need a clear answer to where its data lives, what a save acknowledgement means, and what happens when the optional connection fails.

Source, dates, and what this page verifies

This page describes the supplied project account and related full article. Original publication dates remain attached to the source version. No production repository, message provider, Google Sheet, user account or game service was accessed.

The diagrams are newly implemented teaching examples, not screenshots or re-creations of complete application logic. They use fictional data and memory-only state; the CraftNet fragment carries bounded numeric example settings only. Original code excerpts are preserved and are not executed as production code here.

Technical clarifications and source corrections are identified in the article and in docs/batch-3/SOURCE-REVIEW.md. They do not constitute a new application security, performance or deployment audit.

Open the original published source ↗