diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md index a601c67..ef56c7a 100644 --- a/DEPLOYMENT_GUIDE.md +++ b/DEPLOYMENT_GUIDE.md @@ -122,6 +122,10 @@ configuration, a formula result, and key deletion. The prepared justification text is in `MARKETPLACE_LISTING.md`; the exact branding values, scope justifications, and recording sequence are in `OAUTH_VERIFICATION.md`. +For the proven draft-install activation sequence, custom-function registration +diagnostics, reviewer-fixture rules, and submission notes, follow +`GOOGLE_MARKETPLACE_PLAYBOOK.md`. + ## 5. Install a test Editor add-on Create a blank spreadsheet containing no customer data. diff --git a/GOOGLE_MARKETPLACE_PLAYBOOK.md b/GOOGLE_MARKETPLACE_PLAYBOOK.md new file mode 100644 index 0000000..185efa7 --- /dev/null +++ b/GOOGLE_MARKETPLACE_PLAYBOOK.md @@ -0,0 +1,269 @@ +# Google Sheets add-on launch playbook + +This playbook captures the production lessons from the original +`OilPriceAPI for Google Sheets™` submission so later OilPriceAPI add-ons can +reuse the proven path without repeating the same diagnostics. + +Use this together with: + +- `DEPLOYMENT_GUIDE.md` for the release commands and Google configuration; +- `MARKETPLACE_LISTING.md` for reviewed listing copy and assets; +- `OAUTH_VERIFICATION.md` for the current submission record; +- `PORTFOLIO.md` for product order and acquisition measurement. + +## Proven sequence + +Follow this order. Reordering the Google steps creates ambiguous failures and +avoidable review delays. + +1. Create a standalone Apps Script project and link it to a standard, + organization-controlled Google Cloud project. +2. Deploy the exact reviewed source, run automated validation, smoke it with a + non-customer key, and create an immutable Apps Script version. +3. Configure identical functional scopes in the Apps Script manifest, Google + Auth Platform Data Access page, and Workspace Marketplace SDK. +4. Create public app-specific homepage, privacy, terms, support, and opt-out + URLs. Verify the authorized domain through the same organization-controlled + account used for the Cloud project. +5. Configure OAuth as External and move it to In production. +6. Publish and verify OAuth branding before trying to prepare Data Access + verification. +7. Add the publisher or dedicated reviewer account as a Marketplace draft + tester. +8. Install the real Marketplace draft, select **Use in this document**, refresh + the spreadsheet, and run the installed-add-on smoke. +9. Record a clean OAuth review video from that installed draft. +10. Submit OAuth Data Access verification once and preserve the receipt. +11. Wait for OAuth approval before submitting or resubmitting the Marketplace + listing when Google has explicitly required that sequence. +12. Update the Marketplace integration to the exact tested Apps Script version, + submit once, and repeat the installed-add-on smoke after publication. + +## Scope contract + +Every OilPriceAPI Sheets add-on currently needs only these three functional +scopes: + +```text +https://www.googleapis.com/auth/spreadsheets.currentonly +https://www.googleapis.com/auth/script.external_request +https://www.googleapis.com/auth/script.container.ui +``` + +Google may display `userinfo.email` and `userinfo.profile` as identity defaults. +Do not add those defaults to product logic, and do not add Drive-wide access. + +Use this combined Data Access justification: + +> OilPriceAPI for Google Sheets uses spreadsheets.currentonly to read +> user-selected inputs and write formulas and requested market-data tables only +> in the spreadsheet where the add-on is open; it does not request Drive-wide +> access. It uses script.external_request only to send HTTPS GET requests to +> api.oilpriceapi.com for market data the user explicitly requests. Requests +> include the user's stored OilPriceAPI key and selected commodity identifiers +> or filters; general spreadsheet contents are not transferred. It uses +> script.container.ui to display the add-on menu, API-key sidebar, +> price-selection dialog, help alerts, diagnostics, and recovery actions inside +> Google Sheets. These are the narrowest scopes available: currentonly limits +> spreadsheet access to the active document, external_request is required to +> call the OilPriceAPI service, and container.ui is required for the in-sheet +> sidebar and dialogs. + +## The two Google test paths are not equivalent + +An Apps Script Editor add-on test deployment can prove menu, sidebar, dialog, +authorization, and API request behavior. It did not register the custom +function namespace in fresh test documents during this submission: + +- sidebar key save and Fetch Latest Available Prices worked; +- formula autocomplete was absent; +- even a service-free diagnostic custom function returned `#NAME?`; +- no formula execution appeared in Apps Script logs. + +That combination proves a host registration problem, not an OilPriceAPI +authentication failure. + +The real Marketplace draft install registered the functions correctly only +after this sequence: + +1. Add the Google account as a Marketplace draft tester. +2. Install the draft from its Marketplace page. +3. Open a new blank spreadsheet. +4. Select **Extensions → Add-ons → Manage add-ons**. +5. Open the OilPriceAPI add-on menu and select **Use in this document**. +6. Refresh the spreadsheet. + +After that, autocomplete appeared and `OILPRICE_PRICE`, `OILPRICE_CODES`, and +the other functions executed normally. + +Do not spend API debugging time on a `#NAME?` result when: + +- the sidebar fetch works; +- formula autocomplete is absent; and +- Apps Script shows no formula invocation. + +First confirm installed-add-on activation and namespace registration. + +## Credential-context lesson + +Sidebar handlers and custom functions can run in different Apps Script +authorization contexts. A key that works in the sidebar does not by itself +prove a formula can retrieve it. + +The production runtime therefore stores: + +- a primary copy in document properties; and +- a compatibility copy in the spreadsheet owner's user properties, keyed by + spreadsheet ID. + +The two prototype candidates also wrote an unscoped user-property key. Current +packages intentionally ignore that value because it cannot be tied to its +original spreadsheet. Prototype testers must save the key again in every +spreadsheet they continue to use. + +The spreadsheet owner should configure the key. The OilPriceAPI account email +does not need to match the Google account email. The sidebar must expose only +configured/not-configured state; never return the stored credential to HTML, +cells, diagnostics, logs, screenshots, URLs, or issue trackers. + +Test all four credential states: + +1. no key; +2. valid key; +3. invalid or revoked key; +4. deleted key with diagnostic state cleared. + +## Avoid duplicate add-on contexts + +Simultaneous Apps Script test deployments and Marketplace draft installs can +produce duplicate extension entries and make results impossible to interpret. + +Before the final smoke: + +1. remove obsolete Apps Script test deployments; +2. uninstall stale draft installs if necessary; +3. confirm only one OilPriceAPI add-on is installed; +4. create a new spreadsheet; +5. enable the installed draft for that document; +6. refresh before testing formulas. + +Name test spreadsheets with the product, version, and path, for example: +`OPA Marketplace Draft v11 Smoke`. + +## Reviewer fixture + +Create one persistent synthetic reviewer user and API key per listing. + +- Use a clearly synthetic, non-customer email identity. +- Suppress lifecycle email, billing, and product analytics for the fixture. +- Give it only the datasets and conservative quota needed for review. +- Use a descriptive key name with the product and date. +- Verify one valid request and one invalid-key request before recording. +- Keep the key active through review, but never commit or post it. +- Deliver it only through Google's private reviewer field or existing review + email thread. + +Do not rotate or delete the fixture while a case is open unless exposure is +suspected. + +## Review video checklist + +The reviewer video and the public marketing video are different assets. +Keep the reviewer video Unlisted when it shows an account identifier or +unverified-app screen. + +Before recording: + +- enable Do Not Disturb; +- close unrelated tabs and applications; +- disable visible password-manager prompts; +- use a blank spreadsheet and synthetic key; +- set the OAuth consent language to English; +- record at 1080p or better; +- rehearse the exact flow once. + +Show: + +1. Marketplace installation and the complete OAuth consent flow; +2. the exact app name and every requested permission; +3. **Use in this document** activation; +4. the sidebar and masked/configured credential state; +5. successful connection and response-schema validation; +6. custom-function autocomplete and a live numeric result; +7. sidebar batch fetch writing into the active spreadsheet; +8. source, unit, timestamp, and freshness metadata. + +Before upload, sample the full rendered video at frequent intervals. Remove: + +- raw keys, passwords, cookies, or clipboard contents; +- browser autofill and password-manager overlays; +- notification panels; +- Terminal windows and unrelated tabs; +- macOS capture controls; +- customer or personal spreadsheet data. + +Upload the reviewer copy as Unlisted and verify the URL while signed out before +submitting it. + +## Google Auth submission notes + +- Branding must be published and verified before Data Access verification is + enabled. +- The Verification Center may describe unverified branding as “not being shown + to users” even when the underlying fields are complete. Open Branding, + publish it, then return to Verification Center. +- The Data Access form accepts one combined justification of up to 1,000 + characters and a reviewer-accessible YouTube URL. +- Confirm that Restricted scopes shows no rows. +- Save Data Access changes before returning to Verification Center. +- Submit once. Repeated submissions can trigger a cooldown and delay review. +- Preserve the exact confirmation text, date, screenshot, case ID, and review + email thread. + +For the original add-on, Google confirmed receipt on July 30, 2026, said the +first Trust and Safety email should arrive within 3–5 days, and warned that the +full review can take up to 4–6 weeks. + +## What can and cannot be automated + +Automate: + +- manifest and scope parity; +- packaging and secret scans; +- API success and negative-path tests; +- asset dimensions; +- legal/disclosure URL checks; +- immutable version records; +- API-client attribution and activation measurement. + +Keep a real-account smoke for: + +- Marketplace draft tester installation; +- **Use in this document** activation; +- custom-function registration and autocomplete; +- OAuth consent presentation; +- Google review submission and receipt capture. + +Google does not expose every Marketplace draft-install or custom-function +registration behavior through public Apps Script or Sheets APIs. API-written +formulas cannot bypass an unregistered custom-function namespace. + +## Per-add-on evidence packet + +Create a separate packet for every portfolio product: + +- Cloud project ID and numeric project number; +- Apps Script project ID and immutable version; +- Git commit and validation output; +- exact three-scope comparison; +- public homepage, privacy, terms, support, and opt-out URLs; +- dedicated reviewer fixture record; +- installed-draft smoke spreadsheet; +- real listing screenshots; +- unlisted OAuth review video; +- OAuth receipt and case thread; +- Marketplace receipt and published listing URL; +- post-publication smoke and log review. + +Do not reuse screenshots, detailed listing descriptions, reviewer keys, Cloud +projects, OAuth identities, or Apps Script projects across portfolio listings. diff --git a/OAUTH_VERIFICATION.md b/OAUTH_VERIFICATION.md index b178e8d..ffc6f03 100644 --- a/OAUTH_VERIFICATION.md +++ b/OAUTH_VERIFICATION.md @@ -97,8 +97,14 @@ and a non-customer test spreadsheet and OilPriceAPI key. 11. Choose Delete API Key and show that the stored-key and diagnostic states are cleared. -The recording must not expose an API key, Google account identifier, customer -data, browser password manager, clipboard contents, or unrelated tabs. +The recording must not expose an API key, customer data, browser password +manager, clipboard contents, or unrelated tabs. Use a dedicated demo Google +account because its identifier can appear as part of the required consent +screen, and keep that reviewer copy Unlisted. + +The completed reviewer video is `https://youtu.be/FakNSmBddhE`. Keep it +Unlisted. Create a different sanitized public acquisition video using +`YOUTUBE_PROMOTION.md`. ## Future release order diff --git a/PORTFOLIO.md b/PORTFOLIO.md index e2a73d4..405d0dc 100644 --- a/PORTFOLIO.md +++ b/PORTFOLIO.md @@ -4,6 +4,10 @@ This repository contains five distinct Google Workspace Marketplace candidates. They share a security and release runtime, but each product creates a different finished workbook for a different buyer and workflow. +All five now have immutable Apps Script 1.0.0 candidates. See +`PORTFOLIO_SUBMISSION_READINESS.md` for the exact Script IDs, versions, +remaining Google Console evidence, and hold point. + | Product | Primary buyer | Activated outcome | Acquisition keyword wedge | | --- | --- | --- | --- | | Crack Spread Lab by OilPriceAPI | Refinery analyst or energy trader | Live refinery-margin workbook with history and sensitivity | crack spread spreadsheet | @@ -29,6 +33,8 @@ Every release package must pass `npm run validate`. Editor add-on smoke test and its OAuth configuration is In production. - The five listings must not reuse screenshots or detailed descriptions. Shared infrastructure is acceptable; duplicate user experiences are not. +- Follow the installed-draft, OAuth, reviewer-fixture, and evidence sequence in + `GOOGLE_MARKETPLACE_PLAYBOOK.md` for every candidate. ## Measurement contract @@ -37,13 +43,16 @@ views**, segmented by product. 1. Listing discovery and install counts come from Marketplace SDK Analytics. 2. A successful product build makes an authenticated OilPriceAPI request with - `X-OilPriceAPI-Client: /`. API logs can therefore count + `X-API-Client: /`. API logs can therefore count first data activation without collecting spreadsheet contents. 3. Sidebar signup links use: `utm_source=workspace_marketplace`, `utm_medium=addon`, and a unique `utm_campaign`. 4. Signup and paid conversion are joined to that campaign in the existing first-party attribution pipeline. +5. Public tutorial links use the YouTube UTM contract in + `YOUTUBE_PROMOTION.md`, allowing YouTube sessions, signups, and first + activations to be compared with Marketplace acquisition. The add-ons do not send spreadsheet contents, formulas, cell values, Google account identifiers, or API keys for analytics. @@ -58,6 +67,17 @@ npm run portfolio:verify npm run validate ``` +Account-independent release verification: + +```bash +npm run test:portfolio:live +npm run portfolio:verify:remote +npm run portfolio:verify:links +``` + +The live smoke requires `OILPRICEAPI_KEY` to be set to a synthetic non-customer +test credential. It never prints the credential. + Deployable Apps Script roots are written to `portfolio/dist//`. Run clasp from the selected product directory so its `.claspignore` exposes only `Code.gs`, `Sidebar.html`, and `appsscript.json`. @@ -71,5 +91,11 @@ Run clasp from the selected product directory so its `.claspignore` exposes only 5. Gas Spread Monitor The first two have the clearest non-overlapping search intent and quickest -time-to-value. Start their test deployments first, measure activation, then -promote the strongest funnel before submitting the next Marketplace listing. +time-to-value. Their prior immutable versions are superseded; cut and remotely +verify new versions from the exact merged source before preparing the +Cloud/OAuth/Marketplace draft for Crack Spread Lab first, measure activation, +then promote the strongest funnel before submitting the next listing. + +Do not submit all five simultaneously. Complete the original add-on's OAuth +review, then take Crack Spread Lab through the full installed-draft smoke and +evidence packet before cloning the operational sequence for the next product. diff --git a/PORTFOLIO_SUBMISSION_READINESS.md b/PORTFOLIO_SUBMISSION_READINESS.md new file mode 100644 index 0000000..008d22a --- /dev/null +++ b/PORTFOLIO_SUBMISSION_READINESS.md @@ -0,0 +1,94 @@ +# Google Workspace portfolio submission readiness + +Reviewed: August 11, 2026 + +## Release candidates + +All five products have distinct code, titles, workflows, Apps Script projects, +listing copy, reviewer guides, least-privilege manifests, activation headers, +UTM campaigns, and graphic asset sets. None has been submitted to Google +Workspace Marketplace. The July immutable versions are retained as historical +receipts but contain the retired unscoped credential fallback and old client +header; a new immutable version is required for every current package. + +| Rollout | Product | Apps Script candidate | Planned Cloud project | Remaining account-bound evidence | +| --- | --- | --- | --- | --- | +| 1 | Crack Spread Lab by OilPriceAPI | [Script](https://script.google.com/d/1c2O84bJoprkUtyo8eHb-yYmz1Mtttpo-8miUrKf3o3rzrVXUBfFU8M5C/edit), new immutable version required (previous 3) | `oilpriceapi-crack-spread` | Immutable version, Cloud/OAuth/Marketplace draft, installed-draft smoke, screenshot, video | +| 2 | Fuel Surcharge Studio by OilPriceAPI | [Script](https://script.google.com/d/1Mii2a-nGgRmrnsV1rl_9wElmZfBrmhJgufYPqvDEjvN_s9xTQ8WHRtxN/edit), new immutable version required (previous 4) | `oilpriceapi-fuel-surcharge` | Immutable version, Cloud/OAuth/Marketplace draft, installed-draft smoke, screenshot, video | +| 3 | Energy Curve Builder by OilPriceAPI | [Script](https://script.google.com/d/1q3YQIyE17nv4uNLQV7Pdw3DwKFu8Yrp9Uwylxw6WPmWhwyKzv4xZZDLZ/edit), new immutable version required (previous 1) | `oilpriceapi-energy-curve` | Immutable version, Cloud/OAuth/Marketplace draft, installed-draft smoke, screenshot, video | +| 4 | Bunker Voyage Planner by OilPriceAPI | [Script](https://script.google.com/d/1aQLBBWhyd_ffw1h9gmVFromI-5MwQsGd2v9REoUhXOHziDgbTA2eqY-u/edit), new immutable version required (previous 2) | `oilpriceapi-bunker-voyage` | Immutable version, Cloud/OAuth/Marketplace draft, installed-draft smoke, screenshot, video | +| 5 | Gas Spread Monitor by OilPriceAPI | [Script](https://script.google.com/d/1Od5dLY-A8l-sULQidIuso3rJRjJvWIaI54JZpPQlCVyAk32phv2D1YSU/edit), new immutable version required (previous 2) | `oilpriceapi-gas-spread` | Immutable version, Cloud/OAuth/Marketplace draft, installed-draft smoke, screenshot, video | + +## Verified now + +- The complete shared runtime and every product-specific builder compile. +- Every customer-critical build produces the documented workbook tabs and + editable Google Sheets formulas. +- Missing, revoked, unentitled, rate-limited, timed-out, malformed, and + incomplete API responses produce recovery messages or fail closed. +- The Energy Curve Builder uses the production + `/v1/futures/ice-wti/curve` and `/v1/futures/ice-brent/curve` contracts. +- The July production API smoke built all five workbook models with + source-timestamped non-customer test data; it must be repeated for the current + hardened source. +- Remote verification proved the July immutable versions no longer match the + current hardened packages, so they cannot be submitted. +- All product landing, signup, pricing, privacy, terms, and support links return + successful first-party responses. +- Each application name is within the 50-character Marketplace limit; every + short description is within the 200-character limit. +- Each manifest requests only: + `spreadsheets.currentonly`, `script.external_request`, and + `script.container.ui`. +- Each product has unique 32px and 128px icons and a unique 220×140 card + banner. Screenshots are intentionally not simulated or reused. + +## Remaining console pass + +Repeat this sequence for one product at a time: + +1. Push the exact merged package, cut a new immutable version, and require + `npm run portfolio:verify:remote` to match it byte-for-byte. +2. Create the separate standard Google Cloud project shown in the table. +3. Enable the Apps Script API and Google Workspace Marketplace SDK. +4. Link the standalone Apps Script project to the standard Cloud project's + numeric project number. +5. Configure External OAuth branding using the exact Marketplace application + name, an organization-controlled support contact, the product guide, and the + shared Workspace privacy and terms pages. +6. Add only the three manifest scopes. Keep the project in Testing while the + draft and reviewer fixture are prepared. +7. Configure a Public Editor add-on Marketplace draft with the recorded Script + ID and immutable version. Public visibility is permanent after it is saved. +8. Install the Marketplace draft—not only the Apps Script editor test + deployment—select **Use in this document**, and refresh the blank test + spreadsheet. +9. Run the product's `REVIEWER_GUIDE.md` with a synthetic non-customer API key, + inspect Apps Script executions, and confirm all documented recovery states. +10. Capture a unique, full-bleed 1280×800 PNG from the exact installed build and + record a scope-complete OAuth demonstration video. +11. Stop before OAuth or Marketplace submission until the preceding product's + Google review feedback has been applied across the shared runtime. + +## Hold and rollout decision + +The original OilPriceAPI listing is public and remains the policy canary. Do not +submit these five listings in parallel. Before the next listing: + +1. Convert any reviewer feedback into shared tests and rebuild all pending + packages. +2. Take Crack Spread Lab through OAuth verification and Marketplace review. +3. Measure listing views, installs, attributed signups, and first workbook + activations. +4. Continue in rollout order only if the prior product is policy-clean and the + acquisition funnel is producing qualified activity. + +This sequencing preserves the search-intent experiment without multiplying an +unknown policy or runtime defect across five live listings. + +## Current Google references + +- [Publish an add-on](https://developers.google.com/workspace/add-ons/how-tos/publish-add-on-overview) +- [Create a Marketplace store listing](https://developers.google.com/workspace/marketplace/create-listing) +- [Marketplace app review requirements](https://developers.google.com/workspace/marketplace/about-app-review) +- [OAuth verification requirements](https://support.google.com/cloud/answer/13464321) diff --git a/README.md b/README.md index 11b2a9d..a9bb2e7 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,16 @@ that version in the Marketplace SDK. Prepared listing copy, scope justifications, required screenshot shots, and generated assets are in [MARKETPLACE_LISTING.md](MARKETPLACE_LISTING.md). +The reusable launch and troubleshooting lessons are in +[GOOGLE_MARKETPLACE_PLAYBOOK.md](GOOGLE_MARKETPLACE_PLAYBOOK.md). The separate +public-video acquisition experiment is in +[YOUTUBE_PROMOTION.md](YOUTUBE_PROMOTION.md). + +The five follow-on products and rollout order are in +[PORTFOLIO.md](PORTFOLIO.md). Their immutable Apps Script candidates and +remaining Google Console gates are tracked in +[PORTFOLIO_SUBMISSION_READINESS.md](PORTFOLIO_SUBMISSION_READINESS.md). + ## Canonical links - [Product facts](https://api.oilpriceapi.com/product-facts.json) diff --git a/YOUTUBE_PROMOTION.md b/YOUTUBE_PROMOTION.md new file mode 100644 index 0000000..c839af8 --- /dev/null +++ b/YOUTUBE_PROMOTION.md @@ -0,0 +1,208 @@ +# Public YouTube promotion plan + +## Recommendation + +Create a separate public promotional video for OilPriceAPI. + +The business case is YouTube search discovery, branded visibility, qualified +referral traffic, and measurable signup or workbook activation. Do not model +the description URL as a guaranteed high-authority SEO backlink. Treat any +indexing or link-equity benefit as secondary. + +Keep the OAuth reviewer video Unlisted. It contains review-specific screens and +an account identifier and is not the polished public asset. + +YouTube's own guidance says unique, keyword-relevant titles and descriptions +help viewers find videos through search. Long-form descriptions can contain +clickable external links when the channel has advanced features enabled: + +- `https://support.google.com/youtube/answer/12948449?hl=en` +- `https://support.google.com/youtube/answer/13748639?hl=en` + +## Release sequence + +The original add-on is publicly available in Google Workspace Marketplace. +Keep the two video concepts distinct so workflow discovery and installation +intent remain measurable. + +### Video 1: API workflow tutorial + +Focus on the underlying workflow and OilPriceAPI: + +> How to Pull WTI and Brent Prices into Google Sheets™ | OilPriceAPI Tutorial + +Show a clean spreadsheet, API setup, live values, units, source timestamps, and +freshness. Use a signup or integration-page CTA so this video measures the +underlying data workflow rather than add-on installation. + +### Video 2: Marketplace installation tutorial + +Focus on discovery and installation: + +> OilPriceAPI Google Sheets™ Add-on: Install, Configure, and Build a Live Oil +> Price Sheet + +Show the public Marketplace listing, installation, **Use in this document**, +configuration, formulas, batch fetch, and a finished workbook. + +## Trademark and policy guardrails + +- Use `OilPriceAPI` as the brand and publisher identity. +- Use `Google Sheets™` only to describe compatibility or the demonstrated + workflow. +- Do not use Google branding in a way that implies sponsorship, certification, + endorsement, or ownership. +- Do not place `Google`, `Google Sheets`, or another Google product trademark + in the Marketplace product name for future portfolio products. +- Add this description footer: + +> Google Sheets™ is a trademark of Google LLC. OilPriceAPI is not affiliated +> with or endorsed by Google LLC. + +- Do not claim the add-on is available from Marketplace before Google publishes + the listing. + +## Public recording standard + +Record a new public asset; do not republish the OAuth reviewer video. + +- Use a dedicated demo account and a blank, professionally formatted workbook. +- Show no email address, OAuth warning page, API key, password manager, + notifications, unrelated tabs, or capture controls. +- Use narration or concise on-screen callouts. +- Record at 1080p, 30 fps, and target 4–7 minutes. +- Lead with the finished outcome in the first 10 seconds. +- Demonstrate WTI, Brent, and natural gas, then source timestamp and unit + metadata. +- Include one recovery state, such as a missing key, followed by the successful + next action. +- End with one CTA. + +## Search packaging + +Primary topic: + +`oil price data in Google Sheets` + +Supporting phrases: + +- WTI price Google Sheets; +- Brent crude price spreadsheet; +- oil price API tutorial; +- energy market data spreadsheet; +- commodity price formulas; +- live oil prices in a spreadsheet. + +Use one primary phrase naturally in the title and first two description lines. +Use YouTube Analytics Research to validate the final wording before recording. +Do not stuff tags or repeat exact-match phrases unnaturally. + +Suggested thumbnail: + +- real spreadsheet crop with WTI and Brent values; +- OilPriceAPI droplet mark; +- 3–5 words: `LIVE OIL DATA → SHEETS`; +- no Google logo and no endorsement-style badge. + +Suggested chapters: + +```text +00:00 Live oil-price workbook +00:15 What OilPriceAPI provides +00:40 Configure the spreadsheet +01:30 WTI and Brent formulas +02:30 Units, sources, and timestamps +03:30 Fetch a market-data table +04:30 Common errors and recovery +05:15 Next step +``` + +## Description template + +```text +Pull WTI, Brent, natural-gas, and other energy-market data into Google Sheets™ +with OilPriceAPI. This tutorial shows live price formulas, units, source +timestamps, freshness metadata, and a multi-commodity table. + +Start here: +https://www.oilpriceapi.com/integrations/google-sheets?utm_source=youtube&utm_medium=organic_video&utm_campaign=google_sheets_addon&utm_content=_overview_demo + +Create an OilPriceAPI account: +https://www.oilpriceapi.com/auth/signup?utm_source=youtube&utm_medium=organic_video&utm_campaign=google_sheets_addon&utm_content=_signup_cta + +Documentation: +https://docs.oilpriceapi.com + +Google Sheets™ is a trademark of Google LLC. OilPriceAPI is not affiliated +with or endorsed by Google LLC. +``` + +Confirm that both destination URLs preserve UTM parameters through redirects +before publishing. + +## Measurement contract + +Google Analytics recommends consistent `utm_source`, `utm_medium`, and +`utm_campaign` values so referral sessions appear in Traffic acquisition: + +`https://support.google.com/analytics/answer/10917952?hl=en` + +Use: + +```text +utm_source=youtube +utm_medium=organic_video +utm_campaign=google_sheets_addon +utm_content= +``` + +Track weekly for 90 days: + +| Funnel stage | Metric | Source | +| --- | --- | --- | +| Discovery | Impressions and YouTube Search traffic | YouTube Analytics | +| Packaging | Impression click-through rate | YouTube Analytics | +| Engagement | Average percentage viewed and 30-second retention | YouTube Analytics | +| Intent | Description-link clicks or GA4 sessions | YouTube/GA4 | +| Acquisition | Signups attributed to the UTM campaign | First-party attribution | +| Activation | First successful add-on/API request with product client header | OilPriceAPI logs | +| Revenue | Paid conversions attributed to the campaign | Billing attribution | + +North-star metrics: + +1. activated OilPriceAPI accounts per 1,000 video views; +2. activated workbooks per 100 YouTube-referred landing-page sessions. + +Use a unique `utm_content` for each CTA and video, such as: + +- `overview_demo_description`; +- `install_tutorial_description`; +- `pinned_comment`; +- `channel_profile`. + +Do not put email addresses, Google account IDs, spreadsheet contents, API keys, +or other user data into analytics events. + +## 90-day launch test + +1. Publish one outcome-led tutorial. +2. Add the tracked integration-page link to the first two description lines. +3. Add a tracked signup link below it. +4. Add the integration-page link to the channel profile. +5. Publish one short excerpt that points to the long-form related video; Shorts + description URLs are not clickable. +6. Review YouTube search terms and audience retention after 7 days. +7. Treat Day 30 as the interim review and Day 90 as the final success decision. +8. Compare YouTube-referred signup and activation rates with Marketplace and + organic-search traffic. +9. Produce the next portfolio-product video only if the first video produces + qualified visits or activation signal, not merely views. + +Success threshold for the first experiment: + +- at least 100 qualified landing-page sessions, or +- at least 10 attributed signups, or +- at least 3 first API/add-on activations + +within 90 days. If none occur, revise the topic, thumbnail, CTA, or landing-page +match before scaling the series. diff --git a/assets/marketplace/bunker-voyage-planner/app-icon-128.png b/assets/marketplace/bunker-voyage-planner/app-icon-128.png new file mode 100644 index 0000000..9907743 Binary files /dev/null and b/assets/marketplace/bunker-voyage-planner/app-icon-128.png differ diff --git a/assets/marketplace/bunker-voyage-planner/app-icon-32.png b/assets/marketplace/bunker-voyage-planner/app-icon-32.png new file mode 100644 index 0000000..32e0b34 Binary files /dev/null and b/assets/marketplace/bunker-voyage-planner/app-icon-32.png differ diff --git a/assets/marketplace/bunker-voyage-planner/app-icon.svg b/assets/marketplace/bunker-voyage-planner/app-icon.svg new file mode 100644 index 0000000..de3f230 --- /dev/null +++ b/assets/marketplace/bunker-voyage-planner/app-icon.svg @@ -0,0 +1,6 @@ + + + + + BV + diff --git a/assets/marketplace/bunker-voyage-planner/card-banner-220x140.png b/assets/marketplace/bunker-voyage-planner/card-banner-220x140.png new file mode 100644 index 0000000..02114fa Binary files /dev/null and b/assets/marketplace/bunker-voyage-planner/card-banner-220x140.png differ diff --git a/assets/marketplace/bunker-voyage-planner/card-banner.svg b/assets/marketplace/bunker-voyage-planner/card-banner.svg new file mode 100644 index 0000000..8eaa892 --- /dev/null +++ b/assets/marketplace/bunker-voyage-planner/card-banner.svg @@ -0,0 +1,10 @@ + + + + + + Bunker Voyage + Planner + Purpose-built workflow + by OilPriceAPI + diff --git a/assets/marketplace/bunker-voyage-planner/screenshots/README.md b/assets/marketplace/bunker-voyage-planner/screenshots/README.md new file mode 100644 index 0000000..f36448f --- /dev/null +++ b/assets/marketplace/bunker-voyage-planner/screenshots/README.md @@ -0,0 +1,3 @@ +# Bunker Voyage Planner by OilPriceAPI screenshot evidence + +Capture at least one full-bleed 1280×800 PNG from the exact immutable installed Marketplace draft after completing the real-account smoke in REVIEWER_GUIDE.md. Do not reuse or simulate another product's screenshot. diff --git a/assets/marketplace/crack-spread-lab/app-icon-128.png b/assets/marketplace/crack-spread-lab/app-icon-128.png new file mode 100644 index 0000000..dde8fd2 Binary files /dev/null and b/assets/marketplace/crack-spread-lab/app-icon-128.png differ diff --git a/assets/marketplace/crack-spread-lab/app-icon-32.png b/assets/marketplace/crack-spread-lab/app-icon-32.png new file mode 100644 index 0000000..d3e061b Binary files /dev/null and b/assets/marketplace/crack-spread-lab/app-icon-32.png differ diff --git a/assets/marketplace/crack-spread-lab/app-icon.svg b/assets/marketplace/crack-spread-lab/app-icon.svg new file mode 100644 index 0000000..6d6d0cb --- /dev/null +++ b/assets/marketplace/crack-spread-lab/app-icon.svg @@ -0,0 +1,6 @@ + + + + + 321 + diff --git a/assets/marketplace/crack-spread-lab/card-banner-220x140.png b/assets/marketplace/crack-spread-lab/card-banner-220x140.png new file mode 100644 index 0000000..e3f05ec Binary files /dev/null and b/assets/marketplace/crack-spread-lab/card-banner-220x140.png differ diff --git a/assets/marketplace/crack-spread-lab/card-banner.svg b/assets/marketplace/crack-spread-lab/card-banner.svg new file mode 100644 index 0000000..880d2b6 --- /dev/null +++ b/assets/marketplace/crack-spread-lab/card-banner.svg @@ -0,0 +1,10 @@ + + + + + + Crack Spread + Lab + Purpose-built workflow + by OilPriceAPI + diff --git a/assets/marketplace/crack-spread-lab/screenshots/README.md b/assets/marketplace/crack-spread-lab/screenshots/README.md new file mode 100644 index 0000000..53e01e0 --- /dev/null +++ b/assets/marketplace/crack-spread-lab/screenshots/README.md @@ -0,0 +1,3 @@ +# Crack Spread Lab by OilPriceAPI screenshot evidence + +Capture at least one full-bleed 1280×800 PNG from the exact immutable installed Marketplace draft after completing the real-account smoke in REVIEWER_GUIDE.md. Do not reuse or simulate another product's screenshot. diff --git a/assets/marketplace/energy-curve-builder/app-icon-128.png b/assets/marketplace/energy-curve-builder/app-icon-128.png new file mode 100644 index 0000000..fe262b3 Binary files /dev/null and b/assets/marketplace/energy-curve-builder/app-icon-128.png differ diff --git a/assets/marketplace/energy-curve-builder/app-icon-32.png b/assets/marketplace/energy-curve-builder/app-icon-32.png new file mode 100644 index 0000000..1f3edc3 Binary files /dev/null and b/assets/marketplace/energy-curve-builder/app-icon-32.png differ diff --git a/assets/marketplace/energy-curve-builder/app-icon.svg b/assets/marketplace/energy-curve-builder/app-icon.svg new file mode 100644 index 0000000..4c2ca09 --- /dev/null +++ b/assets/marketplace/energy-curve-builder/app-icon.svg @@ -0,0 +1,6 @@ + + + + + EC + diff --git a/assets/marketplace/energy-curve-builder/card-banner-220x140.png b/assets/marketplace/energy-curve-builder/card-banner-220x140.png new file mode 100644 index 0000000..c880a7b Binary files /dev/null and b/assets/marketplace/energy-curve-builder/card-banner-220x140.png differ diff --git a/assets/marketplace/energy-curve-builder/card-banner.svg b/assets/marketplace/energy-curve-builder/card-banner.svg new file mode 100644 index 0000000..61bd121 --- /dev/null +++ b/assets/marketplace/energy-curve-builder/card-banner.svg @@ -0,0 +1,10 @@ + + + + + + Energy Curve + Builder + Purpose-built workflow + by OilPriceAPI + diff --git a/assets/marketplace/energy-curve-builder/screenshots/README.md b/assets/marketplace/energy-curve-builder/screenshots/README.md new file mode 100644 index 0000000..0bb352a --- /dev/null +++ b/assets/marketplace/energy-curve-builder/screenshots/README.md @@ -0,0 +1,3 @@ +# Energy Curve Builder by OilPriceAPI screenshot evidence + +Capture at least one full-bleed 1280×800 PNG from the exact immutable installed Marketplace draft after completing the real-account smoke in REVIEWER_GUIDE.md. Do not reuse or simulate another product's screenshot. diff --git a/assets/marketplace/fuel-surcharge-studio/app-icon-128.png b/assets/marketplace/fuel-surcharge-studio/app-icon-128.png new file mode 100644 index 0000000..7272012 Binary files /dev/null and b/assets/marketplace/fuel-surcharge-studio/app-icon-128.png differ diff --git a/assets/marketplace/fuel-surcharge-studio/app-icon-32.png b/assets/marketplace/fuel-surcharge-studio/app-icon-32.png new file mode 100644 index 0000000..6ca45c3 Binary files /dev/null and b/assets/marketplace/fuel-surcharge-studio/app-icon-32.png differ diff --git a/assets/marketplace/fuel-surcharge-studio/app-icon.svg b/assets/marketplace/fuel-surcharge-studio/app-icon.svg new file mode 100644 index 0000000..c1c83b8 --- /dev/null +++ b/assets/marketplace/fuel-surcharge-studio/app-icon.svg @@ -0,0 +1,6 @@ + + + + + FS + diff --git a/assets/marketplace/fuel-surcharge-studio/card-banner-220x140.png b/assets/marketplace/fuel-surcharge-studio/card-banner-220x140.png new file mode 100644 index 0000000..8e057c7 Binary files /dev/null and b/assets/marketplace/fuel-surcharge-studio/card-banner-220x140.png differ diff --git a/assets/marketplace/fuel-surcharge-studio/card-banner.svg b/assets/marketplace/fuel-surcharge-studio/card-banner.svg new file mode 100644 index 0000000..73ffb48 --- /dev/null +++ b/assets/marketplace/fuel-surcharge-studio/card-banner.svg @@ -0,0 +1,10 @@ + + + + + + Fuel Surcharge + Studio + Purpose-built workflow + by OilPriceAPI + diff --git a/assets/marketplace/fuel-surcharge-studio/screenshots/README.md b/assets/marketplace/fuel-surcharge-studio/screenshots/README.md new file mode 100644 index 0000000..f567f7a --- /dev/null +++ b/assets/marketplace/fuel-surcharge-studio/screenshots/README.md @@ -0,0 +1,3 @@ +# Fuel Surcharge Studio by OilPriceAPI screenshot evidence + +Capture at least one full-bleed 1280×800 PNG from the exact immutable installed Marketplace draft after completing the real-account smoke in REVIEWER_GUIDE.md. Do not reuse or simulate another product's screenshot. diff --git a/assets/marketplace/gas-spread-monitor/app-icon-128.png b/assets/marketplace/gas-spread-monitor/app-icon-128.png new file mode 100644 index 0000000..36c8073 Binary files /dev/null and b/assets/marketplace/gas-spread-monitor/app-icon-128.png differ diff --git a/assets/marketplace/gas-spread-monitor/app-icon-32.png b/assets/marketplace/gas-spread-monitor/app-icon-32.png new file mode 100644 index 0000000..6da8d54 Binary files /dev/null and b/assets/marketplace/gas-spread-monitor/app-icon-32.png differ diff --git a/assets/marketplace/gas-spread-monitor/app-icon.svg b/assets/marketplace/gas-spread-monitor/app-icon.svg new file mode 100644 index 0000000..832c679 --- /dev/null +++ b/assets/marketplace/gas-spread-monitor/app-icon.svg @@ -0,0 +1,6 @@ + + + + + GS + diff --git a/assets/marketplace/gas-spread-monitor/card-banner-220x140.png b/assets/marketplace/gas-spread-monitor/card-banner-220x140.png new file mode 100644 index 0000000..783d07f Binary files /dev/null and b/assets/marketplace/gas-spread-monitor/card-banner-220x140.png differ diff --git a/assets/marketplace/gas-spread-monitor/card-banner.svg b/assets/marketplace/gas-spread-monitor/card-banner.svg new file mode 100644 index 0000000..1271418 --- /dev/null +++ b/assets/marketplace/gas-spread-monitor/card-banner.svg @@ -0,0 +1,10 @@ + + + + + + Gas Spread + Monitor + Purpose-built workflow + by OilPriceAPI + diff --git a/assets/marketplace/gas-spread-monitor/screenshots/README.md b/assets/marketplace/gas-spread-monitor/screenshots/README.md new file mode 100644 index 0000000..a0e5da5 --- /dev/null +++ b/assets/marketplace/gas-spread-monitor/screenshots/README.md @@ -0,0 +1,3 @@ +# Gas Spread Monitor by OilPriceAPI screenshot evidence + +Capture at least one full-bleed 1280×800 PNG from the exact immutable installed Marketplace draft after completing the real-account smoke in REVIEWER_GUIDE.md. Do not reuse or simulate another product's screenshot. diff --git a/package.json b/package.json index 1238ef3..8aeb5ea 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,12 @@ "scripts": { "test": "node --test test/runtime.test.js test/public-claims.test.js test/secret-scan.test.js", "test:portfolio": "node --test test/portfolio.test.js", + "test:portfolio:live": "node test/portfolio-live-smoke.js", "test:live": "node test/live-smoke.js", "portfolio:build": "node scripts/build-portfolio.js", "portfolio:verify": "node scripts/verify-portfolio.js", + "portfolio:verify:remote": "node scripts/verify-apps-script-releases.js", + "portfolio:verify:links": "node scripts/verify-portfolio-links.js", "assets": "node scripts/generate-marketplace-assets.js", "verify:assets": "node scripts/verify-marketplace-assets.js", "verify:deploy": "node scripts/verify-deploy-package.js", diff --git a/portfolio/dist/bunker-voyage-planner/Code.gs b/portfolio/dist/bunker-voyage-planner/Code.gs index e42c34e..5d6ae2a 100644 --- a/portfolio/dist/bunker-voyage-planner/Code.gs +++ b/portfolio/dist/bunker-voyage-planner/Code.gs @@ -2,6 +2,10 @@ const OPA_PRODUCT = Object.freeze({ "id": "bunker-voyage-planner", "name": "Bunker Voyage Planner by OilPriceAPI", "menu": "Bunker Voyage Planner", + "version": "1.0.0", + "cloudProjectId": "oilpriceapi-bunker-voyage", + "iconMark": "BV", + "brandColor": "#0369A1", "builder": "buildBunkerVoyageWorkbook", "tagline": "Compare port fuel choices and calculate voyage bunker cost.", "landingPath": "/integrations/bunker-voyage-planner", @@ -32,7 +36,7 @@ const OPA_PRODUCT = Object.freeze({ const OPA_API_BASE_URL = 'https://api.oilpriceapi.com/v1'; const OPA_KEY_PROPERTY = 'OILPRICEAPI_KEY'; const OPA_ACTIVATED_PROPERTY = 'OILPRICEAPI_ACTIVATED_AT'; -const OPA_VERSION = '0.1.0'; +const OPA_VERSION = OPA_PRODUCT.version; const OPA_SIGNUP_URL = 'https://www.oilpriceapi.com/auth/signup'; const OPA_ALLOWED_SCOPES = [ 'https://www.googleapis.com/auth/spreadsheets.currentonly', @@ -62,6 +66,7 @@ function showSidebar() { html.productName = OPA_PRODUCT.name; html.tagline = OPA_PRODUCT.tagline; html.builder = OPA_PRODUCT.builder; + html.version = OPA_VERSION; html.signupUrl = signupUrl_(); html.landingUrl = landingUrl_(); SpreadsheetApp.getUi().showSidebar( @@ -89,9 +94,43 @@ function documentProperties_() { } } +function activeSpreadsheetId_() { + try { + const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); + return spreadsheet && typeof spreadsheet.getId === 'function' + ? spreadsheet.getId() + : null; + } catch (error) { + return null; + } +} + +function spreadsheetKeyProperty_() { + const spreadsheetId = activeSpreadsheetId_(); + return spreadsheetId ? `${OPA_KEY_PROPERTY}:${spreadsheetId}` : null; +} + function getApiKey_() { - const properties = documentProperties_(); - return properties ? properties.getProperty(OPA_KEY_PROPERTY) : null; + const documentProperties = documentProperties_(); + const documentKey = documentProperties + ? documentProperties.getProperty(OPA_KEY_PROPERTY) + : null; + if (documentKey) return documentKey; + + // Installed add-on custom functions can run in a separate Apps Script + // authorization context where document properties are unavailable. Keep a + // compatibility copy in owner user properties, namespaced by spreadsheet ID, + // so the key cannot cross into another workbook. + const userProperties = PropertiesService.getUserProperties(); + const spreadsheetKeyProperty = spreadsheetKeyProperty_(); + const spreadsheetKey = spreadsheetKeyProperty + ? userProperties.getProperty(spreadsheetKeyProperty) + : null; + if (spreadsheetKey) return spreadsheetKey; + + // Prototype releases used an unscoped user property. It cannot be tied to a + // source spreadsheet safely, so require the user to save the key again. + return null; } function requireApiKey_() { @@ -105,18 +144,28 @@ function requireApiKey_() { function saveApiKey(apiKey) { const value = typeof apiKey === 'string' ? apiKey.trim() : ''; if (!value || value.length > 512) throw new Error('Enter a valid OilPriceAPI key.'); - const properties = documentProperties_(); - if (!properties) throw new Error('Open the add-on from a spreadsheet before saving a key.'); - properties.setProperty(OPA_KEY_PROPERTY, value); + const documentProperties = documentProperties_(); + const spreadsheetKeyProperty = spreadsheetKeyProperty_(); + if (!documentProperties || !spreadsheetKeyProperty) { + throw new Error('Open the add-on from a spreadsheet before saving a key.'); + } + documentProperties.setProperty(OPA_KEY_PROPERTY, value); + const userProperties = PropertiesService.getUserProperties(); + userProperties.setProperty(spreadsheetKeyProperty, value); + userProperties.deleteProperty(OPA_KEY_PROPERTY); return { success: true, configured: true }; } function deleteApiKey() { - const properties = documentProperties_(); - if (properties) { - properties.deleteProperty(OPA_KEY_PROPERTY); - properties.deleteProperty(OPA_ACTIVATED_PROPERTY); + const documentProperties = documentProperties_(); + if (documentProperties) { + documentProperties.deleteProperty(OPA_KEY_PROPERTY); + documentProperties.deleteProperty(OPA_ACTIVATED_PROPERTY); } + const userProperties = PropertiesService.getUserProperties(); + const spreadsheetKeyProperty = spreadsheetKeyProperty_(); + if (spreadsheetKeyProperty) userProperties.deleteProperty(spreadsheetKeyProperty); + userProperties.deleteProperty(OPA_KEY_PROPERTY); return { success: true, configured: false }; } @@ -153,18 +202,24 @@ function requestJson_(path, apiKey) { if (!normalizedPath.startsWith('/') || normalizedPath.includes('://') || normalizedPath.includes('..')) { throw new Error('Unsupported OilPriceAPI path.'); } - const response = UrlFetchApp.fetch(`${OPA_API_BASE_URL}${normalizedPath}`, { - method: 'get', - headers: { - Authorization: `Token ${apiKey}`, - Accept: 'application/json', - 'X-OilPriceAPI-Client': `${OPA_PRODUCT.activationHeader}/${OPA_VERSION}` - }, - muteHttpExceptions: true - }); + let response; + try { + response = UrlFetchApp.fetch(`${OPA_API_BASE_URL}${normalizedPath}`, { + method: 'get', + headers: { + Authorization: `Token ${apiKey}`, + Accept: 'application/json', + 'X-API-Client': `${OPA_PRODUCT.activationHeader}/${OPA_VERSION}` + }, + muteHttpExceptions: true + }); + } catch (error) { + throw new Error('OilPriceAPI could not be reached. Check the connection and retry.'); + } const status = response.getResponseCode(); if (status === 401) throw new Error('The OilPriceAPI key is invalid or revoked. Replace it in the sidebar.'); if (status === 402 || status === 403) throw new Error('This dataset is not enabled for the account. Review OilPriceAPI pricing or use an entitled key.'); + if (status === 408) throw new Error('The OilPriceAPI request timed out. Retry in a moment.'); if (status === 429) throw new Error('The OilPriceAPI rate or quota limit was reached. Retry later or review the account limit.'); if (status < 200 || status >= 300) throw new Error(`OilPriceAPI returned HTTP ${status}. Retry later.`); let payload; @@ -184,14 +239,19 @@ function priceRecords_(payload) { else if (data && Array.isArray(data.prices)) records = data.prices; else if (data && data.prices && typeof data.prices === 'object') records = Object.values(data.prices); else if (data && typeof data === 'object' && ('price' in data || 'code' in data)) records = [data]; - const normalized = records.map((record) => ({ - code: String(record.code || '').toUpperCase(), - price: Number(record.price), - currency: String(record.currency || ''), - unit: String(record.unit || ''), - source: String(record.source || ''), - timestamp: String(record.created_at || record.as_of || record.timestamp || '') - })); + const normalized = records.map((record) => { + const rawPrice = record && record.price; + return { + code: String(record && record.code || '').toUpperCase(), + price: rawPrice === null || rawPrice === undefined || rawPrice === '' + ? NaN + : Number(rawPrice), + currency: String(record && record.currency || ''), + unit: String(record && record.unit || ''), + source: String(record && record.source || ''), + timestamp: String(record && (record.created_at || record.as_of || record.timestamp) || '') + }; + }); if (!normalized.length || normalized.some((record) => !record.code || !Number.isFinite(record.price))) { throw new Error('OilPriceAPI response is missing a finite market price.'); } @@ -225,7 +285,12 @@ function testConnection() { const probe = OPA_PRODUCT.allowedCodes.length ? latestPrices_([OPA_PRODUCT.allowedCodes[0]])[0] : productConnectionProbe_(); - return { success: true, code: probe.code || probe.contract || 'curve', timestamp: probe.timestamp || '' }; + return { + success: true, + code: probe.code || probe.contract || 'curve', + timestamp: probe.timestamp || '', + message: 'Connection and response schema verified.' + }; } function activateProduct_() { @@ -265,6 +330,9 @@ function marketDataRows_(records) { function calculateVoyageFuelCost_(seaDays, seaConsumption, portDays, portConsumption, vlsfoShare, vlsfoPrice, mgoPrice) { const values = [seaDays, seaConsumption, portDays, portConsumption, vlsfoShare, vlsfoPrice, mgoPrice].map(Number); if (!values.every(Number.isFinite)) throw new Error('Voyage-cost inputs must be finite numbers.'); + if ([values[0], values[1], values[2], values[3], values[5], values[6]].some((value) => value < 0)) { + throw new Error('Voyage days, consumption, and fuel prices must be non-negative.'); + } if (values[4] < 0 || values[4] > 1) throw new Error('VLSFO share must be between 0 and 1.'); const tonnes = (values[0] * values[1]) + (values[2] * values[3]); const blendedPrice = (values[4] * values[5]) + ((1 - values[4]) * values[6]); @@ -308,6 +376,7 @@ function buildBunkerVoyageWorkbook() { ['VLSFO share', 0.9, 'fraction'], ['Total fuel', singapore.tonnes, 'metric tonnes'] ]); + plan.getRange('B10').setFormula('=(B5*B6)+(B7*B8)'); plan.getRange('B9').setNumberFormat('0.0%'); const compare = sheet_('Scenario Compare'); @@ -318,6 +387,13 @@ function buildBunkerVoyageWorkbook() { ['Rotterdam', rotterdam.tonnes, rotterdam.blendedPrice, rotterdam.totalCost, rotterdam.totalCost - Math.min(singapore.totalCost, rotterdam.totalCost, houston.totalCost)], ['Houston', houston.tonnes, houston.blendedPrice, houston.totalCost, houston.totalCost - Math.min(singapore.totalCost, rotterdam.totalCost, houston.totalCost)] ]); + const scenarioFormulas = [5, 6, 7].map((row) => [ + `='Voyage Plan'!$B$10`, + `='Voyage Plan'!$B$9*SUMIFS('Port Prices'!$C:$C,'Port Prices'!$A:$A,A${row},'Port Prices'!$B:$B,"VLSFO")+(1-'Voyage Plan'!$B$9)*SUMIFS('Port Prices'!$C:$C,'Port Prices'!$A:$A,A${row},'Port Prices'!$B:$B,"MGO 0.5%")`, + `=B${row}*C${row}`, + `=D${row}-MIN($D$5:$D$7)` + ]); + compare.getRange(5, 2, 3, 4).setFormulas(scenarioFormulas); compare.getRange(5, 3, 3, 3).setNumberFormat('$#,##0.00'); activateProduct_(); diff --git a/portfolio/dist/bunker-voyage-planner/MARKETPLACE_LISTING.md b/portfolio/dist/bunker-voyage-planner/MARKETPLACE_LISTING.md index 802b66c..724c964 100644 --- a/portfolio/dist/bunker-voyage-planner/MARKETPLACE_LISTING.md +++ b/portfolio/dist/bunker-voyage-planner/MARKETPLACE_LISTING.md @@ -1,13 +1,17 @@ # Bunker Voyage Planner by OilPriceAPI — Marketplace listing -Status: release package validated locally. Do not claim Marketplace availability until Google approves and publishes this distinct listing. +Status: pre-submission package. Do not claim Marketplace availability until Google approves and publishes this distinct listing. ## App details - Application name: `Bunker Voyage Planner by OilPriceAPI` +- OAuth application name: `Bunker Voyage Planner by OilPriceAPI` - Category: Productivity - Pricing: Free of charge with paid features - Developer: `OilPriceAPI` +- Version: `1.0.0` +- Product guide: `https://www.oilpriceapi.com/integrations/bunker-voyage-planner` +- Pricing details: `https://www.oilpriceapi.com/pricing` Short description: @@ -21,7 +25,7 @@ Detailed description: > > The API key is stored in Apps Script document properties for the current spreadsheet. It is not written to cells, URLs, diagnostics, or browser-side HTML. Requests send only reviewed market identifiers and a product/version header used for first-party activation and reliability measurement. Spreadsheet contents, formulas, and cell values are not sent for analytics. > -> Google Sheets™ is a trademark of Google LLC. +> Google Sheets™ is a trademark of Google LLC. Bunker Voyage Planner by OilPriceAPI is not affiliated with or endorsed by Google LLC. ## Distinct workflow @@ -32,7 +36,7 @@ Detailed description: ## Measurement - Marketplace discovery: Google Workspace Marketplace SDK impressions and install events. -- Activation: first successful OilPriceAPI request carrying `X-OilPriceAPI-Client: bunker-voyage-planner/`. +- Activation: first successful OilPriceAPI request carrying `X-API-Client: bunker-voyage-planner/`. - Signup: `utm_source=workspace_marketplace&utm_medium=addon&utm_campaign=bunker_voyage_planner`. - North-star rate: activated workbooks per 100 listing views. @@ -46,10 +50,26 @@ Detailed description: Google's mandatory `userinfo.email` and `userinfo.profile` defaults may appear in Cloud configuration. Product behavior does not use them. No Drive-wide scope is requested. +Combined Data Access justification: + +> This Editor add-on uses spreadsheets.currentonly only to create and format the named workbook tabs in the spreadsheet where the user explicitly runs Bunker Voyage Planner; it cannot browse or modify other spreadsheets. It uses script.external_request only to send the user-configured OilPriceAPI key and the product's reviewed market identifiers to api.oilpriceapi.com after the user selects Test connection or Build. It uses script.container.ui only to add the Bunker Voyage Planner menu and display its key-management sidebar, About dialog, build status, and recovery messages. No narrower scopes support these visible features. The add-on does not read Google account identity, browse Drive, send spreadsheet contents for analytics, or place API keys in cells or URLs. + ## Support links - Product guide: `https://www.oilpriceapi.com/integrations/bunker-voyage-planner` - Signup: `https://www.oilpriceapi.com/auth/signup?utm_source=workspace_marketplace&utm_medium=addon&utm_campaign=bunker_voyage_planner` +- Pricing: `https://www.oilpriceapi.com/pricing` - Privacy: `https://www.oilpriceapi.com/privacy/workspace-addons` - Terms: `https://www.oilpriceapi.com/terms/workspace-addons` - Support: `https://www.oilpriceapi.com/support` +- Setup: `https://www.oilpriceapi.com/integrations/bunker-voyage-planner` +- Help: `https://www.oilpriceapi.com/integrations/bunker-voyage-planner` +- Report issue: `https://www.oilpriceapi.com/support` + +## Submission assets + +- 32px icon: `assets/marketplace/bunker-voyage-planner/app-icon-32.png` +- 128px icon: `assets/marketplace/bunker-voyage-planner/app-icon-128.png` +- Card banner: `assets/marketplace/bunker-voyage-planner/card-banner-220x140.png` +- Screenshot: capture the exact immutable installed build at 1280×800 after the real-account smoke test; do not reuse another product's screenshot. +- OAuth demo video: record the exact OAuth consent screen, requested scopes, key configuration, connection test, and workbook build for this product. diff --git a/portfolio/dist/bunker-voyage-planner/REVIEWER_GUIDE.md b/portfolio/dist/bunker-voyage-planner/REVIEWER_GUIDE.md new file mode 100644 index 0000000..54d7889 --- /dev/null +++ b/portfolio/dist/bunker-voyage-planner/REVIEWER_GUIDE.md @@ -0,0 +1,49 @@ +# Bunker Voyage Planner by OilPriceAPI — reviewer guide + +Status: pre-submission. Evidence that depends on a real installed Marketplace draft is explicitly gated below. + +## Reviewer prerequisites + +- Google Workspace host: Google Sheets +- Access model: the reviewer installs the Editor add-on and supplies an OilPriceAPI test key provided privately in the Marketplace review instructions. +- No Google account identity match is required. The OilPriceAPI key may belong to a different email address. +- The reviewer fixture must be synthetic, non-customer, active for the review window, and entitled to: `VLSFO_SGSIN_USD`, `MGO_05S_SGSIN_USD`, `VLSFO_NLRTM_USD`, `MGO_05S_NLRTM_USD`, `VLSFO_USHOU_USD`, `MGO_05S_USHOU_USD`. + +## End-to-end review flow + +1. Install the unpublished Marketplace draft in a blank spreadsheet. +2. In **Extensions → Add-ons → Manage add-ons**, select Bunker Voyage Planner by OilPriceAPI and choose **Use in this document**. +3. Refresh the spreadsheet once. +4. Open **Extensions → Bunker Voyage Planner → Configure OilPriceAPI key**. +5. Paste the private reviewer key and select **Save key**. +6. Select **Test connection** and confirm the green success result. +7. Select **Build workbook**. +8. Confirm these product-specific tabs exist: `Voyage Plan`, `Port Prices`, `Scenario Compare`. +9. Confirm source timestamps and units are visible and no API key is written to a cell. +10. Delete the stored key and confirm the sidebar reports that no key is configured. + +## OAuth scope demonstration + +- `spreadsheets.currentonly`: steps 7–9 create and format only the current spreadsheet. +- `script.external_request`: steps 6–7 request only the reviewed OilPriceAPI market data. +- `script.container.ui`: steps 2–7 display the add-on menu, sidebar, status, and recovery UI. + +## Evidence to attach + +- Apps Script ID: `1aQLBBWhyd_ffw1h9gmVFromI-5MwQsGd2v9REoUhXOHziDgbTA2eqY-u` +- Immutable Apps Script version: `New immutable version required` +- Previous immutable version (superseded): `2` +- Marketplace draft install: Pending the separate Cloud project and draft integration. +- Clean test spreadsheet URL: Provide privately after installed-draft smoke. +- OAuth demo video URL: Record after the exact OAuth branding and scopes are configured. +- Reviewed 1280×800 screenshot: Capture after installed-draft smoke. +- Reviewer test credential: Provide privately; never commit. + +## Expected recovery behavior + +- Missing key: asks the reviewer to configure a key or create an account. +- Invalid or revoked key: asks the reviewer to replace it in the sidebar. +- Dataset unavailable: explains the entitlement problem and points to pricing. +- Rate or quota limit: asks the reviewer to retry later or review the account limit. +- Timeout/network failure: asks the reviewer to check the connection and retry. +- Malformed or incomplete success response: rejects the data instead of building a misleading workbook. diff --git a/portfolio/dist/bunker-voyage-planner/SUBMISSION_CHECKLIST.md b/portfolio/dist/bunker-voyage-planner/SUBMISSION_CHECKLIST.md new file mode 100644 index 0000000..af08a33 --- /dev/null +++ b/portfolio/dist/bunker-voyage-planner/SUBMISSION_CHECKLIST.md @@ -0,0 +1,40 @@ +# Bunker Voyage Planner by OilPriceAPI — pre-submission checklist + +Target Cloud project ID: `oilpriceapi-bunker-voyage` + +## Automated package — complete before deployment + +- [x] Distinct product title without a Google trademark +- [x] Product version `1.0.0` +- [x] Three least-privilege Apps Script scopes +- [x] First-party API fetch allowlist +- [x] Spreadsheet-scoped key storage with installed-custom-function compatibility +- [x] Product-specific activation header and UTM campaign +- [x] Trademark attribution and non-affiliation wording +- [x] Product-specific listing copy, reviewer guide, and graphic assets +- [x] Pricing, privacy, terms, setup, help, and support links +- [x] Unit, negative-path, claims, package, asset, and secret validation + +## Google account work — prepare, but do not submit yet + +- [x] Create or confirm the standalone Apps Script project +- [ ] Push the exact validated package and create an immutable version +- [ ] Create the separate standard Google Cloud project `oilpriceapi-bunker-voyage` +- [ ] Enable Apps Script API and Google Workspace Marketplace SDK +- [ ] Link the Apps Script project to the standard Cloud project +- [ ] Configure External OAuth branding with the exact application name +- [ ] Add only the three manifest scopes and reconcile the Marketplace integration scopes +- [ ] Keep OAuth in Testing while preparing the installed draft +- [ ] Configure a Public Marketplace draft; remember visibility cannot be changed after saving +- [ ] Install the Marketplace draft, select **Use in this document**, refresh, and run the reviewer guide +- [ ] Inspect Apps Script executions for new exceptions, retries, or unexpected 4xx/5xx responses +- [ ] Capture a unique full-bleed 1280×800 screenshot from the immutable installed build +- [ ] Record the OAuth demonstration video for this exact app name and scope set +- [ ] Move OAuth to In production and prepare verification only when this product reaches its rollout slot + +## Hold point + +- [ ] Confirm the original OilPriceAPI listing has cleared OAuth and Marketplace review +- [ ] Apply any reviewer feedback to the shared runtime and every pending package +- [ ] Rebuild, create a new immutable version if anything changed, and repeat the real-account smoke +- [ ] Submit this listing only in the approved rollout order diff --git a/portfolio/dist/bunker-voyage-planner/Sidebar.html b/portfolio/dist/bunker-voyage-planner/Sidebar.html index 16b08ff..0934979 100644 --- a/portfolio/dist/bunker-voyage-planner/Sidebar.html +++ b/portfolio/dist/bunker-voyage-planner/Sidebar.html @@ -7,6 +7,7 @@ h1 { color:#0f3557; font-size:20px; line-height:1.2; margin:0 0 6px; } .muted { color:#627d98; font-size:12px; } .card { background:#f0f7ff; border:1px solid #b8d8f0; border-radius:8px; margin:16px 0; padding:14px; } + .notice { background:#f8fafc; border-left:4px solid #0f6cbf; margin:16px 0; padding:10px 12px; } input { border:1px solid #9fb3c8; border-radius:5px; box-sizing:border-box; padding:10px; width:100%; } button { background:#0f6cbf; border:0; border-radius:5px; color:white; cursor:pointer; font-weight:700; margin-top:9px; padding:10px 12px; width:100%; } button.secondary { background:#526d82; } @@ -22,19 +23,30 @@

OilPriceAPI key

Stored only in this spreadsheet's Apps Script document properties. The stored value is never displayed.

+

Prototype testers must save the key again in each spreadsheet so the retired unscoped credential is never reused elsewhere.

-
+
+
+ Data use +

+ Your key is stored for this spreadsheet in Apps Script properties and is never displayed after saving. + Only the key and reviewed market identifiers are sent to OilPriceAPI when you test or build. + Spreadsheet contents, formulas, cell values, and Google account identity are not sent for analytics. + Your OilPriceAPI account email does not need to match your Google account. +

+
+

Create an API key · Product guide

- Product requests include a product/version HTTP header so OilPriceAPI can measure activation and reliability. - No spreadsheet contents, formulas, or cell values are sent for analytics. + Privacy · + Terms · + Support

-

Create an API key · Product guide

-

Google Sheets™ is a trademark of Google LLC.

+

Version · Google Sheets™ is a trademark of Google LLC. Not affiliated with or endorsed by Google LLC.