TryAITryAITry TryAI

Shared chat

Create Google Sheets PWA

i need to turn this google sheet into pwa app, i will need to upload and display two photos, vin photo and vehicle photo. service column will need a drop down for 5 services. Basic recon, price $30. Full LMC $130. LMC Detail SUV $170. Superior recon $60. Superior recon SUV $85
Claude Fable 5

Great use case — you can do this with Google Apps Script as the backend (writes rows to your Sheet, saves photos to Drive) and a simple PWA frontend (installable, works on a phone camera). Here's a complete working setup.

Architecture

  • Google Sheet = your database (columns: Timestamp, VIN, Service, Price, VIN Photo URL, Vehicle Photo URL)
  • Apps Script Web App = API that receives form data + photos, saves photos to a Drive folder, appends a row
  • PWA = HTML form with camera/photo inputs, service dropdown with auto-price, manifest + service worker so it installs like an app

1. Apps Script backend (Code.gs)

In your Sheet: Extensions → Apps Script, paste this:

javascript
const SHEET_NAME = 'Sheet1';const FOLDER_ID = 'YOUR_DRIVE_FOLDER_ID'; // create a Drive folder for photos function doPost(e) {  const data = JSON.parse(e.postData.contents);  const folder = DriveApp.getFolderById(FOLDER_ID);   const vinUrl = saveImage(folder, data.vinPhoto, `VIN_${data.vin}`);  const vehicleUrl = saveImage(folder, data.vehiclePhoto, `VEHICLE_${data.vin}`);   const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);  sheet.appendRow([new Date(), data.vin, data.service, data.price, vinUrl, vehicleUrl]);   return ContentService.createTextOutput(JSON.stringify({ ok: true }))    .setMimeType(ContentService.MimeType.JSON);} function saveImage(folder, base64, name) {  if (!base64) return '';  const bytes = Utilities.base64Decode(base64.split(',')[1]);  const blob = Utilities.newBlob(bytes, 'image/jpeg', name + '.jpg');  const file = folder.createFile(blob);  file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);  return `https://drive.google.com/uc?id=${file.getId()}`;} // Optional: fetch rows so the app can display entriesfunction doGet() {  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);  const rows = sheet.getDataRange().getValues();  return ContentService.createTextOutput(JSON.stringify(rows))    .setMimeType(ContentService.MimeType.JSON);}

Then: Deploy → New deployment → Web app → Execute as Me, access Anyone. Copy the web app URL.

2. PWA frontend (index.html)

Host this on GitHub Pages, Netlify, or Vercel (free — PWAs need HTTPS):

html
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Recon Tracker</title><link rel="manifest" href="manifest.json"><meta name="theme-color" content="#1a73e8"><style>  body { font-family: system-ui; max-width: 480px; margin: auto; padding: 16px; }  label { display: block; margin-top: 12px; font-weight: 600; }  input, select, button { width: 100%; padding: 10px; margin-top: 4px; box-sizing: border-box; }  img.preview { max-width: 100%; margin-top: 8px; border-radius: 8px; display: none; }  button { background: #1a73e8; color: #fff; border: 0; border-radius: 8px; margin-top: 16px; font-size: 16px; }  #price { font-size: 20px; font-weight: bold; margin-top: 8px; }</style></head><body><h2>Vehicle Recon Entry</h2> <label>VIN</label><input id="vin" placeholder="Enter VIN"> <label>Service</label><select id="service">  <option value="" data-price="0">Select service…</option>  <option data-price="30">Basic Recon — $30</option>  <option data-price="130">Full LMC — $130</option>  <option data-price="170">LMC Detail SUV — $170</option>  <option data-price="60">Superior Recon — $60</option>  <option data-price="85">Superior Recon SUV — $85</option></select><div id="price"></div> <label>VIN Photo</label><input type="file" id="vinPhoto" accept="image/*" capture="environment"><img class="preview" id="vinPreview"> <label>Vehicle Photo</label><input type="file" id="vehiclePhoto" accept="image/*" capture="environment"><img class="preview" id="vehiclePreview"> <button id="submit">Save Entry</button> <script>const API_URL = 'YOUR_APPS_SCRIPT_WEB_APP_URL'; const serviceEl = document.getElementById('service');serviceEl.onchange = () => {  const p = serviceEl.selectedOptions[0].dataset.price;  document.getElementById('price').textContent = p > 0 ? `Price: $${p}` : '';}; function readAsCompressedBase64(file, previewId) {  return new Promise(resolve => {    const img = new Image();    img.onload = () => {      const canvas = document.createElement('canvas');      const scale = Math.min(1, 1280 / img.width);      canvas.width = img.width * scale;      canvas.height = img.height * scale;      canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);      const dataUrl = canvas.toDataURL('image/jpeg', 0.7);      const prev = document.getElementById(previewId);      prev.src = dataUrl; prev.style.display = 'block';      resolve(dataUrl);    };    img.src = URL.createObjectURL(file);  });} let vinPhotoData = '', vehiclePhotoData = '';document.getElementById('vinPhoto').onchange = async e =>  vinPhotoData = await readAsCompressedBase64(e.target.files[0], 'vinPreview');document.getElementById('vehiclePhoto').onchange = async e =>  vehiclePhotoData = await readAsCompressedBase64(e.target.files[0], 'vehiclePreview'); document.getElementById('submit').onclick = async () => {  const vin = document.getElementById('vin').value.trim();  const opt = serviceEl.selectedOptions[0];  if (!vin || !opt.dataset.price || opt.dataset.price === '0') {    alert('Enter VIN and select a service'); return;  }  const btn = document.getElementById('submit');  btn.disabled = true; btn.textContent = 'Saving…';  await fetch(API_URL, {    method: 'POST',    body: JSON.stringify({      vin,      service: opt.textContent.split('—')[0].trim(),      price: opt.dataset.price,      vinPhoto: vinPhotoData,      vehiclePhoto: vehiclePhotoData    })  });  btn.disabled = false; btn.textContent = 'Save Entry';  alert('Saved!');  location.reload();}; if ('serviceWorker' in navigator) navigator.serviceWorker.register('sw.js');</script></body></html>

3. PWA files

manifest.json:

json
{  "name": "Recon Tracker",  "short_name": "Recon",  "start_url": ".",  "display": "standalone",  "background_color": "#ffffff",  "theme_color": "#1a73e8",  "icons": [    { "src": "icon-192.png", "sizes": "192x192", "type": "image/png" },    { "src": "icon-512.png", "sizes": "512x512", "type": "image/png" }  ]}

sw.js:

javascript
const CACHE = 'recon-v1';self.addEventListener('install', e => {  e.waitUntil(caches.open(CACHE).then(c => c.addAll(['.', 'index.html', 'manifest.json'])));});self.addEventListener('fetch', e => {  e.respondWith(caches.match(e.request).then(r => r || fetch(e.request)));});

Setup checklist

  1. Create a Drive folder for photos → copy its ID into FOLDER_ID
  2. Paste Code.gs into Apps Script → deploy as web app → copy URL into API_URL
  3. Upload the 3 frontend files + two icon PNGs to GitHub Pages/Netlify
  4. Open on your phone → "Add to Home Screen" → it installs as an app

Notes:

  • capture="environment" opens the rear camera directly on phones — perfect for VIN shots.
  • Photos are compressed to ~1280px JPEG before upload to keep things fast.
  • Want offline queueing (save entries with no signal, sync later)? That's an IndexedDB addition to the service worker — let me know and I'll extend it.

If you share your sheet's exact column headers, I'll adjust the appendRow order to match.

Claude Fable 53.5k tok116 tok/sTTFT 5.2s35.7s
how would I display actual photos, reduced size image instead of links?
Claude Fable 5

You have two places to do this — in the Google Sheet itself and in the app's list view. Here's both.

1. Show thumbnails in the Google Sheet

The trick: drive.google.com/uc?id=... links often don't render in Sheets' IMAGE() formula. Use Drive's thumbnail endpoint instead — it also gives you the reduced size for free:

https://drive.google.com/thumbnail?id=FILE_ID&sz=w200

Update saveImage() in your Apps Script to write an IMAGE() formula instead of a bare URL:

javascript
function saveImage(folder, base64, name) {  if (!base64) return '';  const bytes = Utilities.base64Decode(base64.split(',')[1]);  const blob = Utilities.newBlob(bytes, 'image/jpeg', name + '.jpg');  const file = folder.createFile(blob);  file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);  return file.getId(); // return just the ID now}

Then in doPost, write the formulas with setFormula instead of plain appendRow:

javascript
function doPost(e) {  const data = JSON.parse(e.postData.contents);  const folder = DriveApp.getFolderById(FOLDER_ID);   const vinId = saveImage(folder, data.vinPhoto, `VIN_${data.vin}`);  const vehicleId = saveImage(folder, data.vehiclePhoto, `VEHICLE_${data.vin}`);   const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);  const row = sheet.getLastRow() + 1;   sheet.getRange(row, 1, 1, 4).setValues([[new Date(), data.vin, data.service, data.price]]);   if (vinId) sheet.getRange(row, 5).setFormula(    `=IMAGE("https://drive.google.com/thumbnail?id=${vinId}&sz=w200")`);  if (vehicleId) sheet.getRange(row, 6).setFormula(    `=IMAGE("https://drive.google.com/thumbnail?id=${vehicleId}&sz=w200")`);   // optional: store raw IDs in hidden columns G/H so the app can still fetch them  sheet.getRange(row, 7, 1, 2).setValues([[vinId, vehicleId]]);   // make the row tall enough to see the thumbnails  sheet.setRowHeight(row, 80);   return ContentService.createTextOutput(JSON.stringify({ ok: true }))    .setMimeType(ContentService.MimeType.JSON);}

Tips:

  • sz=w200 = 200px wide. Use w400 for bigger, w100 for smaller.
  • IMAGE(url) defaults to "fit to cell," so the row height / column width controls display size. IMAGE(url, 4, 80, 80) forces an exact 80×80 px if you prefer.
  • Keeping the raw file IDs in columns G/H means your app doesn't have to parse formulas.

2. Show thumbnails in the PWA (list/gallery view)

If you add a list screen that reads from doGet(), render the IDs as <img> tags using the same thumbnail URL, with a click-through to the full image:

javascript
async function loadEntries() {  const rows = await (await fetch(API_URL)).json();  const list = document.getElementById('entries');  list.innerHTML = rows.slice(1).map(r => {    const [ts, vin, service, price, , , vinId, vehicleId] = r;    const thumb = id => id      ? `<a href="https://drive.google.com/file/d/${id}/view" target="_blank">           <img src="https://drive.google.com/thumbnail?id=${id}&sz=w200" loading="lazy">         </a>`      : '';    return `<div class="card">      <strong>${vin}</strong> — ${service} ($${price})      <div class="thumbs">${thumb(vinId)}${thumb(vehicleId)}</div>    </div>`;  }).join('');}

With a little CSS:

css
.card { border: 1px solid #ddd; border-radius: 8px; padding: 10px; margin-top: 10px; }.thumbs img { width: 120px; height: 90px; object-fit: cover; border-radius: 6px; margin-right: 8px; }
  • loading="lazy" keeps the list fast when you have many rows.
  • Tapping a thumbnail opens the full-resolution photo in Drive.
  • Photos must be shared "Anyone with link" (your setSharing call already does this) or the thumbnails will show as broken images.

One caveat: if you have existing rows with the old uc?id= URLs, you can convert them once with a formula like:

=IMAGE(SUBSTITUTE(E2, "uc?id=", "thumbnail?id=") & "&sz=w200")

Want me to add the full gallery/list screen to the PWA so you can browse and search past entries by VIN?

Claude Fable 52.0k tok115 tok/sTTFT 6.7s23.9s
ok i have this working, i was able to take and upload pictures, input vin number and select service i do not see the results on the google sheet though. how would i generate the results to the spreadsheet, or what am i missing ?
GPT-5.5

Most likely one of these is missing:

  1. Your Apps Script is saving photos to Drive but not appending the row
  2. SHEET_NAME does not match your actual Google Sheet tab name
  3. The script is not connected to the correct spreadsheet
  4. You edited Apps Script but did not redeploy a new Web App version
  5. The PWA is using the wrong Apps Script URL
  6. The request is failing silently

The safest fix is to use the Spreadsheet ID directly instead of relying on getActiveSpreadsheet().


Use this Apps Script backend

In Apps Script, replace your backend with this version.

Change these 3 values:

javascript
const SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID';const SHEET_NAME = 'Sheet1';const FOLDER_ID = 'YOUR_DRIVE_FOLDER_ID';

Full code:

javascript
const SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID';const SHEET_NAME = 'Sheet1';const FOLDER_ID = 'YOUR_DRIVE_FOLDER_ID'; const SERVICES = {  'Basic Recon': 30,  'Full LMC': 130,  'LMC Detail SUV': 170,  'Superior Recon': 60,  'Superior Recon SUV': 85}; function doPost(e) {  try {    const data = JSON.parse(e.postData.contents);     const ss = SpreadsheetApp.openById(SPREADSHEET_ID);    const sheet = ss.getSheetByName(SHEET_NAME);     if (!sheet) {      throw new Error(`Sheet tab not found: ${SHEET_NAME}`);    }     setupHeadersIfNeeded(sheet);     const folder = DriveApp.getFolderById(FOLDER_ID);     const vin = data.vin || '';    const service = data.service || '';    const price = SERVICES[service] || data.price || '';     const vinPhotoId = saveImage(folder, data.vinPhoto, `VIN_${vin}_${Date.now()}`);    const vehiclePhotoId = saveImage(folder, data.vehiclePhoto, `VEHICLE_${vin}_${Date.now()}`);     const row = sheet.getLastRow() + 1;     sheet.getRange(row, 1, 1, 8).setValues([[      new Date(),      vin,      service,      price,      vinPhotoId,      vehiclePhotoId,      '',      ''    ]]);     if (vinPhotoId) {      sheet.getRange(row, 7).setFormula(        `=IMAGE("https://drive.google.com/thumbnail?id=${vinPhotoId}&sz=w200")`      );    }     if (vehiclePhotoId) {      sheet.getRange(row, 8).setFormula(        `=IMAGE("https://drive.google.com/thumbnail?id=${vehiclePhotoId}&sz=w200")`      );    }     sheet.setRowHeight(row, 100);    sheet.setColumnWidth(7, 120);    sheet.setColumnWidth(8, 120);     return jsonResponse({      ok: true,      message: 'Saved to sheet',      row,      vin,      service,      price    });   } catch (err) {    return jsonResponse({      ok: false,      error: err.message    });  }} function saveImage(folder, base64, name) {  if (!base64) return '';   const contentType = base64.substring(    base64.indexOf(':') + 1,    base64.indexOf(';')  );   const bytes = Utilities.base64Decode(base64.split(',')[1]);  const blob = Utilities.newBlob(bytes, contentType, name + '.jpg');   const file = folder.createFile(blob);  file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);   return file.getId();} function setupHeadersIfNeeded(sheet) {  if (sheet.getLastRow() === 0) {    sheet.appendRow([      'Timestamp',      'VIN',      'Service',      'Price',      'VIN Photo File ID',      'Vehicle Photo File ID',      'VIN Photo',      'Vehicle Photo'    ]);  }} function doGet() {  return jsonResponse({    ok: true,    message: 'Apps Script web app is running'  });} function jsonResponse(obj) {  return ContentService    .createTextOutput(JSON.stringify(obj))    .setMimeType(ContentService.MimeType.JSON);}

Important: get the correct Spreadsheet ID

Your Google Sheet URL looks like this:

https://docs.google.com/spreadsheets/d/SPREADSHEET_ID_HERE/edit

Copy only the long ID between /d/ and /edit.

Example:

javascript
const SPREADSHEET_ID = '1abcDEFghiJKLmnopQRSTUVWXYZ123456789';

Important: SHEET_NAME means the bottom tab name

This is not the file name.

If your bottom tab says:

Recon

Then use:

javascript
const SHEET_NAME = 'Recon';

If it says:

Sheet1

Then use:

javascript
const SHEET_NAME = 'Sheet1';

Your frontend submit code should look like this

Make sure your PWA is sending data to the Apps Script URL.

javascript
document.getElementById('submit').onclick = async () => {  const vin = document.getElementById('vin').value.trim();  const serviceEl = document.getElementById('service');  const selectedOption = serviceEl.selectedOptions[0];   const service = selectedOption.value;  const price = selectedOption.dataset.price;   if (!vin) {    alert('Enter VIN');    return;  }   if (!service) {    alert('Select a service');    return;  }   if (!vinPhotoData) {    alert('Take VIN photo');    return;  }   if (!vehiclePhotoData) {    alert('Take vehicle photo');    return;  }   const btn = document.getElementById('submit');  btn.disabled = true;  btn.textContent = 'Saving...';   try {    const response = await fetch(API_URL, {      method: 'POST',      body: JSON.stringify({        vin,        service,        price,        vinPhoto: vinPhotoData,        vehiclePhoto: vehiclePhotoData      })    });     const result = await response.json();     if (!result.ok) {      throw new Error(result.error || 'Unknown error');    }     alert(`Saved to Google Sheet. Row ${result.row}`);     document.getElementById('vin').value = '';    serviceEl.value = '';    document.getElementById('price').textContent = '';    document.getElementById('vinPhoto').value = '';    document.getElementById('vehiclePhoto').value = '';    document.getElementById('vinPreview').style.display = 'none';    document.getElementById('vehiclePreview').style.display = 'none';     vinPhotoData = '';    vehiclePhotoData = '';   } catch (err) {    alert('Save failed: ' + err.message);  }   btn.disabled = false;  btn.textContent = 'Save Entry';};

Your service dropdown should use clean value names:

html
<select id="service">  <option value="" data-price="0">Select service...</option>  <option value="Basic Recon" data-price="30">Basic Recon — $30</option>  <option value="Full LMC" data-price="130">Full LMC — $130</option>  <option value="LMC Detail SUV" data-price="170">LMC Detail SUV — $170</option>  <option value="Superior Recon" data-price="60">Superior Recon — $60</option>  <option value="Superior Recon SUV" data-price="85">Superior Recon SUV — $85</option></select>

Critical step: redeploy after editing Apps Script

After changing the Apps Script code:

  1. Click Deploy
  2. Click Manage deployments
  3. Click the pencil/edit icon
  4. Under Version, choose New version
  5. Click Deploy
  6. Copy the Web App URL again if needed

Your PWA should use the URL ending in:

/exec

Not:

/dev

Example in your PWA:

javascript
const API_URL = 'https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec';

How to check what is failing

In Apps Script:

  1. Open your Apps Script project
  2. Click Executions on the left
  3. Submit a test entry from the PWA
  4. Refresh Executions
  5. Open the latest run

If it failed, you will usually see one of these:

Wrong sheet tab name

Sheet tab not found: Sheet1

Fix:

javascript
const SHEET_NAME = 'Your Actual Tab Name';

Wrong spreadsheet ID

Exception: Unexpected error while getting the method or property openById

Fix SPREADSHEET_ID.

Wrong Drive folder ID

Exception: Unexpected error while getting the method or property getFolderById

Fix FOLDER_ID.

Permission issue

Redeploy the Web App as:

  • Execute as: Me
  • Who has access: Anyone

What should appear in the Sheet

The Sheet should generate rows like this:

TimestampVINServicePriceVIN Photo File IDVehicle Photo File IDVIN PhotoVehicle Photo
date/time123456789Basic Recon30file IDfile IDthumbnailthumbnail

Columns G and H will show actual reduced-size images, not just links.


Most likely issue in your case

If photos are uploading but the Sheet is blank, the most likely problem is this line from the older code:

javascript
SpreadsheetApp.getActiveSpreadsheet()

That can fail or point to nothing if the script is not properly bound to the Sheet.

Use this instead:

javascript
SpreadsheetApp.openById(SPREADSHEET_ID)

That forces Apps Script to write to the exact Google Sheet you want.

GPT-5.52.7k tok106 tok/sTTFT 5.4s30.9s
stuck on saving
GPT-5.5

If it gets stuck on “Saving…”, your button is being disabled, but the fetch() request is either failing or never reaching your success/reset code.

Most common causes:

  • Apps Script URL is wrong or still using old deployment
  • Apps Script permissions/deployment not set correctly
  • JavaScript error after clicking Save
  • CORS issue with Apps Script
  • Photo file is too large
  • Your code does not have a proper catch/finally block to reset the button

Use this safer submit code.

Replace your submit button code with this

javascript
document.getElementById('submit').onclick = async () => {  const vin = document.getElementById('vin').value.trim();  const serviceEl = document.getElementById('service');  const selectedOption = serviceEl.selectedOptions[0];   const service = selectedOption.value;  const price = selectedOption.dataset.price;   const btn = document.getElementById('submit');   if (!vin) {    alert('Enter VIN');    return;  }   if (!service) {    alert('Select a service');    return;  }   if (!vinPhotoData) {    alert('Take VIN photo');    return;  }   if (!vehiclePhotoData) {    alert('Take vehicle photo');    return;  }   btn.disabled = true;  btn.textContent = 'Saving...';   const payload = {    vin,    service,    price,    vinPhoto: vinPhotoData,    vehiclePhoto: vehiclePhotoData  };   try {    console.log('Sending payload:', {      vin,      service,      price,      vinPhotoSize: vinPhotoData.length,      vehiclePhotoSize: vehiclePhotoData.length    });     const response = await fetch(API_URL, {      method: 'POST',      body: JSON.stringify(payload)    });     const text = await response.text();    console.log('Raw response:', text);     const result = JSON.parse(text);     if (!result.ok) {      throw new Error(result.error || 'Save failed');    }     alert(`Saved successfully to row ${result.row}`);     document.getElementById('vin').value = '';    serviceEl.value = '';    document.getElementById('price').textContent = '';    document.getElementById('vinPhoto').value = '';    document.getElementById('vehiclePhoto').value = '';     document.getElementById('vinPreview').style.display = 'none';    document.getElementById('vehiclePreview').style.display = 'none';     vinPhotoData = '';    vehiclePhotoData = '';   } catch (err) {    console.error('Save error:', err);    alert('Save failed: ' + err.message);  } finally {    btn.disabled = false;    btn.textContent = 'Save Entry';  }};

This is important because the finally section will always reset the button, even if saving fails.


If it still gets stuck, use this Apps Script-friendly version

Google Apps Script sometimes causes frontend CORS issues. If you see a CORS error in the browser console, change the fetch to this:

javascript
await fetch(API_URL, {  method: 'POST',  mode: 'no-cors',  body: JSON.stringify(payload)}); alert('Submitted. Check the Google Sheet.');

So the middle part becomes:

javascript
try {  await fetch(API_URL, {    method: 'POST',    mode: 'no-cors',    body: JSON.stringify(payload)  });   alert('Submitted. Check the Google Sheet.'); } catch (err) {  console.error('Save error:', err);  alert('Save failed: ' + err.message);} finally {  btn.disabled = false;  btn.textContent = 'Save Entry';}

With mode: 'no-cors', the app can send the data, but it cannot read the response from Apps Script. So you will not get result.row, but the row should still be added to the Sheet.


Check these 5 things

1. Make sure your API URL ends in /exec

In your PWA:

javascript
const API_URL = 'https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec';

Not:

javascript
/dev

Use /exec.


2. Open the API URL directly

Paste your Apps Script web app URL into your browser.

You should see something like:

json
{  "ok": true,  "message": "Apps Script web app is running"}

If you get an authorization error, the deployment settings are wrong.


3. Redeploy Apps Script after every backend change

In Apps Script:

  1. Click Deploy
  2. Click Manage deployments
  3. Click the pencil/edit icon
  4. Choose New version
  5. Click Deploy

Make sure deployment is:

  • Execute as: Me
  • Who has access: Anyone

4. Check Apps Script executions

In Apps Script, click Executions on the left.

Then submit from your app.

If the request reached Apps Script, you should see a new execution.

If it failed, open it and look for the error.

Common errors:

Sheet tab not found

Fix your SHEET_NAME.

Cannot call DriveApp.getFolderById

Fix your FOLDER_ID.

Cannot call SpreadsheetApp.openById

Fix your SPREADSHEET_ID.


5. Make sure photos are compressed

If photos are too large, the upload may freeze or fail. Your compression function should be like this:

javascript
function readAsCompressedBase64(file, previewId) {  return new Promise(resolve => {    const img = new Image();     img.onload = () => {      const canvas = document.createElement('canvas');       const maxWidth = 1000;      const scale = Math.min(1, maxWidth / img.width);       canvas.width = img.width * scale;      canvas.height = img.height * scale;       canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);       const dataUrl = canvas.toDataURL('image/jpeg', 0.6);       const preview = document.getElementById(previewId);      preview.src = dataUrl;      preview.style.display = 'block';       resolve(dataUrl);    };     img.src = URL.createObjectURL(file);  });}

This keeps uploads much smaller.


Quick fix I would try first

Change your fetch() to this:

javascript
await fetch(API_URL, {  method: 'POST',  mode: 'no-cors',  body: JSON.stringify(payload)}); alert('Submitted. Check the Google Sheet.');

Then check the Google Sheet and Apps Script Executions.

If a row appears, the issue was the frontend trying to read the Apps Script response.

GPT-5.52.0k tok121 tok/sTTFT 4.1s20.6s
failed but no errors shown in executions
GPT-5.5

If it failed but nothing appears in Apps Script → Executions, then the request is not reaching your Apps Script web app at all.

So this is probably not a spreadsheet/write problem yet. It is one of these:

  • Wrong API_URL
  • Web app not deployed as /exec
  • PWA is still using old cached JavaScript
  • Button click code is failing before fetch()
  • CORS/redirect issue
  • Service worker is serving an old version of your app

Do these checks in order.


1. Make sure your URL is the Web App /exec URL

In your frontend:

javascript
const API_URL = 'https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec';

It should not be:

javascript
https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/dev

And it should not be your Google Sheet URL.


2. Open the API URL directly in browser

Paste the API_URL directly into your browser.

If your Apps Script has this:

javascript
function doGet() {  return ContentService    .createTextOutput(JSON.stringify({      ok: true,      message: 'Apps Script web app is running'    }))    .setMimeType(ContentService.MimeType.JSON);}

You should see:

json
{"ok":true,"message":"Apps Script web app is running"}

If you do not see that, your deployment is the issue.


3. Redeploy correctly

In Apps Script:

  1. Click Deploy
  2. Click Manage deployments
  3. Click the pencil/edit icon
  4. Choose New version
  5. Set:
   - **Execute as:** `Me`   - **Who has access:** `Anyone`
  1. Click Deploy

Then copy the Web App URL again.

Use that URL in your PWA.


4. Your PWA may be using an old cached version

Because this is a PWA, the service worker may be serving your old JavaScript even after you edit the file.

For testing, temporarily unregister the service worker.

Add this near the bottom of your index.html:

javascript
if ('serviceWorker' in navigator) {  navigator.serviceWorker.getRegistrations().then(registrations => {    registrations.forEach(registration => registration.unregister());  });}

Then reload the app completely.

On your phone, you may also need to:

  • Delete the PWA from home screen
  • Clear browser cache/site data
  • Reopen the site
  • Re-add to home screen later

This is a very common reason it keeps saying “Saving…” with old code.


5. Add a visible debug message before fetch()

Replace your save handler temporarily with this simplified debug version:

javascript
document.getElementById('submit').onclick = async () => {  alert('Button clicked');   const btn = document.getElementById('submit');  btn.disabled = true;  btn.textContent = 'Saving...';   const payload = {    vin: document.getElementById('vin').value.trim(),    service: document.getElementById('service').value,    price: document.getElementById('service').selectedOptions[0].dataset.price,    vinPhoto: vinPhotoData,    vehiclePhoto: vehiclePhotoData  };   alert('About to send to Apps Script');   try {    await fetch(API_URL, {      method: 'POST',      mode: 'no-cors',      body: JSON.stringify(payload)    });     alert('Request sent. Check Google Sheet and Apps Script Executions.');   } catch (err) {    alert('Fetch failed: ' + err.message);  } finally {    btn.disabled = false;    btn.textContent = 'Save Entry';  }};

Now test again.

What the alerts mean

If you do not see:

Button clicked

Then your button handler is not connected.

If you see:

Button clicked

but not:

About to send to Apps Script

Then your JavaScript is breaking before fetch().

If you see:

Request sent

but still see no execution in Apps Script, then your API_URL or deployment is wrong, or the service worker is still serving old code.


6. Add a basic doPost test

Temporarily replace your Apps Script with this simple version:

javascript
const SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID';const SHEET_NAME = 'Sheet1'; function doGet() {  return ContentService    .createTextOutput(JSON.stringify({      ok: true,      message: 'Apps Script web app is running'    }))    .setMimeType(ContentService.MimeType.JSON);} function doPost(e) {  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);  const sheet = ss.getSheetByName(SHEET_NAME);   sheet.appendRow([    new Date(),    'POST RECEIVED',    e.postData ? e.postData.contents.substring(0, 100) : 'No post data'  ]);   return ContentService    .createTextOutput(JSON.stringify({      ok: true,      message: 'Post received'    }))    .setMimeType(ContentService.MimeType.JSON);}

Then redeploy as a new version.

Test from the PWA again.

If this works, you should see a row like:

Timestamp | POST RECEIVED | {"vin":"..."}

Then we know the frontend is reaching Apps Script.


7. Add this test button to your HTML

This removes photos from the equation and tests only the connection.

Add this button:

html
<button id="testApi" type="button">Test API</button>

Then add this JavaScript:

javascript
document.getElementById('testApi').onclick = async () => {  alert('Testing API...');   try {    await fetch(API_URL, {      method: 'POST',      mode: 'no-cors',      body: JSON.stringify({        test: true,        time: new Date().toISOString()      })    });     alert('Test sent. Check Google Sheet and Apps Script Executions.');   } catch (err) {    alert('Test failed: ' + err.message);  }};

Click Test API.

If this does not create an Apps Script execution, the problem is definitely:

  • wrong API_URL
  • bad deployment settings
  • cached frontend
  • button JavaScript not running

8. Make sure your button is not submitting a form

If your button is inside a <form>, the page may be reloading before the upload completes.

Change the button to:

html
<button id="submit" type="button">Save Entry</button>

Not just:

html
<button id="submit">Save Entry</button>

This one small issue can cause “Saving…” problems.


9. Remove Content-Type: application/json if you added it

For Apps Script web apps, avoid this during testing:

javascript
headers: {  'Content-Type': 'application/json'}

Use this instead:

javascript
await fetch(API_URL, {  method: 'POST',  mode: 'no-cors',  body: JSON.stringify(payload)});

Or:

javascript
await fetch(API_URL, {  method: 'POST',  body: JSON.stringify(payload)});

Do not add custom headers yet.


Most likely fix

Based on “failed but no errors shown in Executions,” I would first check these:

  1. Your PWA is still using an old cached version.
  2. Your API_URL is not the /exec deployment URL.
  3. Your Save button is inside a form and needs type="button".
  4. You edited Apps Script but did not redeploy as a new version.

Start by adding this:

html
<button id="submit" type="button">Save Entry</button>

Then unregister the service worker, redeploy Apps Script, and test with the simple doPost above.

GPT-5.52.3k tok146 tok/sTTFT 8.7s24.3s
Script function not found: doGet
GPT-5.5

That message means your Apps Script Web App URL is working, but Apps Script cannot find a function named:

javascript
doGet()

When you open the Web App URL in a browser, Google runs doGet().
When your PWA submits data, Google runs doPost().

So you need both functions in your Apps Script.

Add this doGet() to your Apps Script

Paste this anywhere in your Apps Script file, outside of all other functions:

javascript
function doGet(e) {  return ContentService    .createTextOutput(JSON.stringify({      ok: true,      message: 'Apps Script web app is running'    }))    .setMimeType(ContentService.MimeType.JSON);}

Then your Web App URL should show:

json
{"ok":true,"message":"Apps Script web app is running"}

Make sure your file has this structure

Your Apps Script should look generally like this:

javascript
const SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID';const SHEET_NAME = 'Sheet1';const FOLDER_ID = 'YOUR_DRIVE_FOLDER_ID'; function doGet(e) {  return ContentService    .createTextOutput(JSON.stringify({      ok: true,      message: 'Apps Script web app is running'    }))    .setMimeType(ContentService.MimeType.JSON);} function doPost(e) {  try {    const data = JSON.parse(e.postData.contents);     const ss = SpreadsheetApp.openById(SPREADSHEET_ID);    const sheet = ss.getSheetByName(SHEET_NAME);     if (!sheet) {      throw new Error('Sheet not found: ' + SHEET_NAME);    }     sheet.appendRow([      new Date(),      data.vin || '',      data.service || '',      data.price || ''    ]);     return ContentService      .createTextOutput(JSON.stringify({        ok: true,        message: 'Saved'      }))      .setMimeType(ContentService.MimeType.JSON);   } catch (err) {    return ContentService      .createTextOutput(JSON.stringify({        ok: false,        error: err.message      }))      .setMimeType(ContentService.MimeType.JSON);  }}

This simplified version only tests writing VIN/service/price to the Sheet. Once this works, you can add the photo logic back in.


Important: redeploy after adding doGet()

After adding the function:

  1. Click Save
  2. Click Deploy
  3. Click Manage deployments
  4. Click the pencil/edit icon
  5. Under Version, choose New version
  6. Click Deploy
  7. Open the Web App URL again

Your URL should end in:

/exec

If you still see the error

Check these things:

1. Function name must be exactly lowercase/camelcase

Correct:

javascript
function doGet(e) {

Wrong:

javascript
function doget(e) {

Wrong:

javascript
function doGET(e) {

Wrong:

javascript
function DoGet(e) {

2. doGet() cannot be inside another function

Wrong:

javascript
function doPost(e) {  function doGet(e) {    // wrong  }}

Correct:

javascript
function doGet(e) {  // correct} function doPost(e) {  // correct}

3. Make sure you are editing the correct Apps Script project

Sometimes there are two script projects:

  • one attached to the Google Sheet
  • one standalone project

The Web App URL may point to a different project than the one you are editing.

To confirm, add this to your doGet():

javascript
function doGet(e) {  return ContentService    .createTextOutput('THIS IS THE CORRECT SCRIPT')    .setMimeType(ContentService.MimeType.TEXT);}

Redeploy as a new version, then open the URL.

If you do not see:

THIS IS THE CORRECT SCRIPT

then your Web App URL is from a different deployment/project.


Next step

First get the Web App URL to show:

json
{"ok":true,"message":"Apps Script web app is running"}

Once that works, your PWA should be able to submit to doPost().

GPT-5.51.1k tok132 tok/sTTFT 3.3s11.3s

This is a shared TryAI chat. Sign in to start your own conversation.

Sign in to TryAI