Free tier: drop spurious BYO key gate; clarify bundled vs BYO
The previous free-tier commit (c0975fe) blocked USE_SERVER_KEY for
unlicensed users on the theory that this protected a "bundled key."
That conflated two different things:
• USE_SERVER_KEY = the user's OWN Gemini key, just stored server-side
via the StartOS configuration action (vs. browser localStorage).
Both paths are BYO — the user pays Google directly either way.
• Bundled key = a future relay where paid users' /api/process requests
are proxied through the operator's service and the operator absorbs
the API cost. Sketched in UPGRADE-DESIGN.md (deleted, in git history)
but NOT YET BUILT.
Blocking USE_SERVER_KEY broke a legitimate flow: a free user installs
the app on their own StartOS, sets their Gemini key via the config
action, then summarizes from the web UI without re-entering it.
This commit:
• Drops the BYO/USE_SERVER_KEY rejection in /api/process. Free users
can use a key from either path; the existing `if (!apiKey)` check
still catches the no-key-anywhere case with a helpful message.
• Reverts the frontend submit-button and handleSubmit checks to the
same key requirement for both tiers (state.apiKey OR state.hasServerKey).
• Drops "bundled API key" from the activation-screen subtitle and
"bring your own Gemini key" from the free-mode banner. Until the
relay is built, paid users still BYO too — promising otherwise in
upgrade copy is misleading.
• Keeps the parts that ARE legitimate free-vs-paid differentiators:
the one-at-a-time concurrency lock and skipping saveToHistory.
Also fixes the `make deploy` redundancy:
• bin/bump-version.sh accepts --from-deploy. When set, if there is no
.release-notes-pending.txt (consumed by a prior bump or never
written), exit 0 without prompting — the current version is already
fresh.
• Makefile passes --from-deploy from the deploy target. Standalone
`make bump` is unchanged (always prompts).
Result: `make bump` then `make deploy` no longer double-prompts. And
calling `make deploy` twice in a row (no new work) is idempotent on
the bump step.
This commit is contained in:
@@ -2,13 +2,17 @@
|
|||||||
include s9pk.mk
|
include s9pk.mk
|
||||||
|
|
||||||
# ── Deploy ─────────────────────────────────────────────────────────────────
|
# ── Deploy ─────────────────────────────────────────────────────────────────
|
||||||
# `make deploy` — interactively bumps the version, builds the x86 .s9pk,
|
# `make deploy` — bump the version (only if .release-notes-pending.txt
|
||||||
# uploads it to FileBrowser, registers with the Start9 registry, and triggers
|
# exists, signalling unshipped changes), build the x86 .s9pk, upload to
|
||||||
# a registry re-index. Reads credentials from .deploy.env — copy
|
# FileBrowser, register with the Start9 registry, and trigger a re-index.
|
||||||
# .deploy.env.example to .deploy.env and fill in your values.
|
# Reads credentials from .deploy.env — copy .deploy.env.example to
|
||||||
|
# .deploy.env and fill in your values.
|
||||||
|
#
|
||||||
|
# The bump step is skipped automatically if you've already run `make bump`
|
||||||
|
# (which consumes the pending-notes file). No more double-prompt.
|
||||||
.PHONY: deploy redeploy bump
|
.PHONY: deploy redeploy bump
|
||||||
deploy:
|
deploy:
|
||||||
@bash bin/bump-version.sh
|
@bash bin/bump-version.sh --from-deploy
|
||||||
@$(MAKE) --no-print-directory x86
|
@$(MAKE) --no-print-directory x86
|
||||||
@bash bin/deploy.sh
|
@bash bin/deploy.sh
|
||||||
|
|
||||||
|
|||||||
@@ -12,16 +12,40 @@
|
|||||||
# This is the convention Claude uses after making code changes. The file is
|
# This is the convention Claude uses after making code changes. The file is
|
||||||
# deleted on a successful bump.
|
# deleted on a successful bump.
|
||||||
#
|
#
|
||||||
|
# Flags:
|
||||||
|
# --from-deploy Treat the absence of .release-notes-pending.txt as the
|
||||||
|
# signal that no new work needs to ship — exit 0 without
|
||||||
|
# bumping. Lets `make deploy` always include this step
|
||||||
|
# without forcing a redundant bump when the user (or a
|
||||||
|
# prior run) already bumped.
|
||||||
|
#
|
||||||
# Run standalone with `make bump`, or as the first step of `make deploy`.
|
# Run standalone with `make bump`, or as the first step of `make deploy`.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
FROM_DEPLOY=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--from-deploy) FROM_DEPLOY=1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
VERSIONS_DIR="$PROJECT_ROOT/startos/versions"
|
VERSIONS_DIR="$PROJECT_ROOT/startos/versions"
|
||||||
INDEX_FILE="$VERSIONS_DIR/index.ts"
|
INDEX_FILE="$VERSIONS_DIR/index.ts"
|
||||||
PENDING_NOTES_FILE="$PROJECT_ROOT/.release-notes-pending.txt"
|
PENDING_NOTES_FILE="$PROJECT_ROOT/.release-notes-pending.txt"
|
||||||
|
|
||||||
|
# When invoked from `make deploy`, treat a missing pending-notes file as
|
||||||
|
# "nothing new to ship — current version is already fresh, skip the bump."
|
||||||
|
# Standalone `make bump` always prompts.
|
||||||
|
if [ "$FROM_DEPLOY" = "1" ] && [ ! -f "$PENDING_NOTES_FILE" ]; then
|
||||||
|
echo ""
|
||||||
|
echo " (No .release-notes-pending.txt — current version is already bumped. Skipping.)"
|
||||||
|
echo ""
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Discover current version ---
|
# --- Discover current version ---
|
||||||
CURRENT_VAR="$(sed -nE "s/.*current:[[:space:]]*(v_[0-9_]+).*/\1/p" "$INDEX_FILE" | head -1)"
|
CURRENT_VAR="$(sed -nE "s/.*current:[[:space:]]*(v_[0-9_]+).*/\1/p" "$INDEX_FILE" | head -1)"
|
||||||
if [ -z "$CURRENT_VAR" ]; then
|
if [ -z "$CURRENT_VAR" ]; then
|
||||||
|
|||||||
+15
-14
@@ -1324,21 +1324,24 @@
|
|||||||
// ── Process ──────────────────────────────────────────────────────────────
|
// ── Process ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
// Free tier requires BYO Gemini key — bundled key is licensed-only.
|
// Both tiers need a Gemini key — either entered in the web UI
|
||||||
const free = !isLicensed();
|
// (state.apiKey, localStorage) or set on the server via the StartOS
|
||||||
const hasKey = free ? !!state.apiKey.trim() : (state.apiKey.trim() || state.hasServerKey);
|
// configuration action (state.hasServerKey). The free-vs-paid line
|
||||||
|
// is concurrency + features, not key sourcing.
|
||||||
|
const hasKey = state.apiKey.trim() || state.hasServerKey;
|
||||||
if (!state.url.trim() || !hasKey) {
|
if (!state.url.trim() || !hasKey) {
|
||||||
if (free && !state.apiKey.trim() && state.url.trim()) {
|
if (!hasKey && state.url.trim()) {
|
||||||
showToast("Free mode needs your own Gemini API key — open Settings to enter one.", "🔑");
|
showToast("Add your Gemini API key in Settings (or via the StartOS configuration action) to start summarizing.", "🔑");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = state.url.trim();
|
const url = state.url.trim();
|
||||||
|
|
||||||
// If already processing — free tier blocks, paid tier queues.
|
// If already processing — free tier blocks (one at a time),
|
||||||
|
// paid tier queues for batch.
|
||||||
if (state.loading) {
|
if (state.loading) {
|
||||||
if (free) {
|
if (!isLicensed()) {
|
||||||
showToast("Free mode handles one video at a time. Wait for the current one to finish.", "⏳");
|
showToast("Free mode handles one video at a time. Wait for the current one to finish.", "⏳");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1625,7 +1628,7 @@
|
|||||||
<p class="activation-sub">
|
<p class="activation-sub">
|
||||||
${loading
|
${loading
|
||||||
? "Checking license…"
|
? "Checking license…"
|
||||||
: "Activate a Keysat license to unlock the full app — library, subscriptions, channel auto-queue, and the bundled API key. Or skip to use free mode (one video at a time, bring your own Gemini key)."
|
: "Activate a Keysat license to unlock the full app — saved library, channel & podcast subscriptions, and auto-queue. Or skip to use free mode (one video at a time, no library)."
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
${loading ? "" : `
|
${loading ? "" : `
|
||||||
@@ -1681,7 +1684,7 @@
|
|||||||
">
|
">
|
||||||
<span style="flex:1; min-width: 220px;">
|
<span style="flex:1; min-width: 220px;">
|
||||||
<strong style="color:#c4b5fd;">Free mode</strong>
|
<strong style="color:#c4b5fd;">Free mode</strong>
|
||||||
· one video at a time, bring your own Gemini key ·
|
· one video at a time ·
|
||||||
no library, no subscriptions
|
no library, no subscriptions
|
||||||
</span>
|
</span>
|
||||||
<a href="${escHtml(buyUrl)}" target="_blank" rel="noopener"
|
<a href="${escHtml(buyUrl)}" target="_blank" rel="noopener"
|
||||||
@@ -1734,12 +1737,10 @@
|
|||||||
const __prevHistoryListEl = document.querySelector(".history-list");
|
const __prevHistoryListEl = document.querySelector(".history-list");
|
||||||
const __prevHistoryScroll = __prevHistoryListEl ? __prevHistoryListEl.scrollTop : 0;
|
const __prevHistoryScroll = __prevHistoryListEl ? __prevHistoryListEl.scrollTop : 0;
|
||||||
const free = !isLicensed();
|
const free = !isLicensed();
|
||||||
const submitNeedsBYO = free; // bundled key is licensed-only
|
// Same key requirement for both tiers today — either web-UI key or
|
||||||
|
// a server-side key set via the StartOS config action.
|
||||||
const submitDisabled = !state.url.trim()
|
const submitDisabled = !state.url.trim()
|
||||||
|| (!isSubscribeUrl(state.url)
|
|| (!isSubscribeUrl(state.url) && !state.apiKey.trim() && !state.hasServerKey);
|
||||||
&& (submitNeedsBYO
|
|
||||||
? !state.apiKey.trim()
|
|
||||||
: (!state.apiKey.trim() && !state.hasServerKey)));
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<!-- Top bar: title + action icons -->
|
<!-- Top bar: title + action icons -->
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
|
|||||||
+6
-9
@@ -2010,17 +2010,14 @@ app.get("/api/processing/log", (req, res) => {
|
|||||||
app.post("/api/process", async (req, res) => {
|
app.post("/api/process", async (req, res) => {
|
||||||
const { url, apiKey: clientKey, model, type: itemType, title: itemTitle, uploadDate: itemUploadDate, episodeId } = req.body;
|
const { url, apiKey: clientKey, model, type: itemType, title: itemTitle, uploadDate: itemUploadDate, episodeId } = req.body;
|
||||||
|
|
||||||
// Free tier: unlicensed users can summarize, but only with their own
|
// Free tier: unlicensed users can summarize one video at a time. They
|
||||||
// Gemini key (no riding on the bundled key) and only one job at a time.
|
// still bring their own Gemini key — same as paid users today; the key
|
||||||
|
// can come from either the StartOS config action (server-side) or the
|
||||||
|
// web UI Settings panel (client-side). The future "bundled key" relay
|
||||||
|
// (paid users' requests proxied through the operator's service) isn't
|
||||||
|
// built yet, so there's nothing here that gates key sourcing by tier.
|
||||||
const isFreeUser = !(LIC.state === "licensed" && LIC.entitlements.has("core"));
|
const isFreeUser = !(LIC.state === "licensed" && LIC.entitlements.has("core"));
|
||||||
if (isFreeUser) {
|
if (isFreeUser) {
|
||||||
if (!clientKey || clientKey === "USE_SERVER_KEY") {
|
|
||||||
return res.status(402).json({
|
|
||||||
error: "byo_key_required",
|
|
||||||
message:
|
|
||||||
"Free mode requires your own Gemini API key. Open Settings to enter one, or upgrade to use the bundled key.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (freeJobInFlight) {
|
if (freeJobInFlight) {
|
||||||
return res.status(409).json({
|
return res.status(409).json({
|
||||||
error: "processing_in_progress",
|
error: "processing_in_progress",
|
||||||
|
|||||||
Reference in New Issue
Block a user