Arslan1997 commited on
Commit
ba40d17
路
1 Parent(s): b612dd5

LETS GOOOOOOOOOOOOOOOOOOOOOOOOO

Browse files
backend/app/routers/artifacts.py CHANGED
@@ -60,8 +60,8 @@ def _target_title(ctx: Ctx, notebook_id: uuid.UUID | None, document_id: uuid.UUI
60
  if not doc.path:
61
  plan = effective_plan(ctx.db, ctx.workspace)
62
  raise plan_limit_error(plan, "indexed_posts",
63
- f"This post is locked. Your plan makes your latest {plan.indexed_posts} posts "
64
- "available; upgrade to use your whole archive.")
65
  return doc.title
66
  if notebook_id:
67
  nb = ctx.db.scalar(select(Notebook).where(Notebook.id == notebook_id,
 
60
  if not doc.path:
61
  plan = effective_plan(ctx.db, ctx.workspace)
62
  raise plan_limit_error(plan, "indexed_posts",
63
+ f"This post is not indexed. Your plan indexes your latest {plan.indexed_posts} "
64
+ "posts; upgrade to use your whole archive.")
65
  return doc.title
66
  if notebook_id:
67
  nb = ctx.db.scalar(select(Notebook).where(Notebook.id == notebook_id,
backend/app/routers/sources.py CHANGED
@@ -35,7 +35,7 @@ def serialize_source(source: Source, doc_count: int = 0, locked_count: int = 0)
35
  "sync_error": source.sync_error,
36
  "last_synced_at": source.last_synced_at.isoformat() if source.last_synced_at else None,
37
  "document_count": doc_count,
38
- # Indexed but beyond the plan's post limit: listed, not usable until the workspace upgrades.
39
  "locked_count": locked_count,
40
  "is_imports": source.feed_url == IMPORTS_FEED,
41
  }
@@ -159,7 +159,7 @@ documents_router = APIRouter(prefix="/api/documents", tags=["documents"])
159
 
160
 
161
  def is_locked(d: Document) -> bool:
162
- """Indexed past the plan's post limit (see ingest.store_locked): visible, but not in the corpus."""
163
  return not d.path and bool((d.metadata_json or {}).get("locked"))
164
 
165
 
@@ -212,10 +212,9 @@ def get_document(document_id: uuid.UUID, ctx: Ctx = Depends(get_ctx)):
212
  raise HTTPException(404, "Post not found")
213
  source = ctx.db.get(Source, doc.source_id) if doc.source_id else None
214
  if is_locked(doc):
215
- # A preview only: the full post is part of what upgrading unlocks.
216
- preview = [ln for ln in (doc.clean_text or "").splitlines() if ln.strip()][:3]
217
  plan = effective_plan(ctx.db, ctx.workspace)
218
- return {**serialize_document(doc, source), "lines": [f"# {doc.title}", "", *preview],
219
  "preview": True, "limit": plan.indexed_posts}
220
  lines: list[str] = []
221
  if doc.path:
 
35
  "sync_error": source.sync_error,
36
  "last_synced_at": source.last_synced_at.isoformat() if source.last_synced_at else None,
37
  "document_count": doc_count,
38
+ # Found but beyond the plan's post limit: listed, not indexed until the workspace upgrades.
39
  "locked_count": locked_count,
40
  "is_imports": source.feed_url == IMPORTS_FEED,
41
  }
 
159
 
160
 
161
  def is_locked(d: Document) -> bool:
162
+ """Past the plan's post limit (see ingest.store_locked): listed with title and link only, not indexed."""
163
  return not d.path and bool((d.metadata_json or {}).get("locked"))
164
 
165
 
 
212
  raise HTTPException(404, "Post not found")
213
  source = ctx.db.get(Source, doc.source_id) if doc.source_id else None
214
  if is_locked(doc):
215
+ # Listed, not indexed: there is no text to show until the workspace upgrades.
 
216
  plan = effective_plan(ctx.db, ctx.workspace)
217
+ return {**serialize_document(doc, source), "lines": [f"# {doc.title}"],
218
  "preview": True, "limit": plan.indexed_posts}
219
  lines: list[str] = []
220
  if doc.path:
backend/app/services/plans.py CHANGED
@@ -41,7 +41,7 @@ PLANS: dict[str, Plan] = {
41
  voice_cloning=False,
42
  brand_kit=False,
43
  features=(
44
- "1 source, whole archive indexed, latest 5 posts available",
45
  "Grounded research chat with citations",
46
  "3 min of audio overviews a month",
47
  "1 min of video a month",
 
41
  voice_cloning=False,
42
  brand_kit=False,
43
  features=(
44
+ "1 source, your latest 5 posts indexed",
45
  "Grounded research chat with citations",
46
  "3 min of audio overviews a month",
47
  "1 min of video a month",
frontend/seo/prerender.ts CHANGED
@@ -182,7 +182,7 @@ function routes(): Rendered[] {
182
  description:
183
  "Notestack pricing: Free ($0), Writer ($24.99 a month, $18.99 billed annually) and Studio ($48.99 a month, $36.99 billed annually). Annual billing saves 24%.",
184
  body: page(
185
- `<h1>Pick your trajectory</h1><ul><li><h2>Free</h2><p>$0. 1 source, whole archive indexed with your latest 5 posts available, 3 audio minutes, 1 video minute, 2 Launch Kits a month.</p></li><li><h2>Writer</h2><p>$24.99 a month ($18.99 a month billed annually). 3 sources, 500 posts, 60 audio minutes, 30 video minutes, 50 Launch Kits.</p></li><li><h2>Studio</h2><p>$48.99 a month ($36.99 a month billed annually). 10 sources, 5,000 posts, 240 audio minutes, 120 video minutes, unlimited Launch Kits.</p></li></ul><p><a href="/auth?mode=signup">Start free</a></p>`,
186
  ),
187
  },
188
  {
 
182
  description:
183
  "Notestack pricing: Free ($0), Writer ($24.99 a month, $18.99 billed annually) and Studio ($48.99 a month, $36.99 billed annually). Annual billing saves 24%.",
184
  body: page(
185
+ `<h1>Pick your trajectory</h1><ul><li><h2>Free</h2><p>$0. 1 source, your latest 5 posts indexed, 3 audio minutes, 1 video minute, 2 Launch Kits a month.</p></li><li><h2>Writer</h2><p>$24.99 a month ($18.99 a month billed annually). 3 sources, 500 posts, 60 audio minutes, 30 video minutes, 50 Launch Kits.</p></li><li><h2>Studio</h2><p>$48.99 a month ($36.99 a month billed annually). 10 sources, 5,000 posts, 240 audio minutes, 120 video minutes, unlimited Launch Kits.</p></li></ul><p><a href="/auth?mode=signup">Start free</a></p>`,
186
  ),
187
  },
188
  {
frontend/seo/sitemap.ts CHANGED
@@ -96,7 +96,7 @@ differs from NotebookLM.
96
  SEO pack, carousel).
97
  - **Publishing:** a Launchpad calendar that auto posts to X, LinkedIn and Bluesky, plus a topic map of rising and
98
  dormant themes and evergreen resurfacing of old posts.
99
- - **Pricing (USD):** Free: 1 source, whole archive indexed with the latest 5 posts available, 3 audio minutes, 1 video minute, 2 launch kits a month.
100
  Writer: $24.99 a month ($18.99 a month billed annually): 3 sources, 500 posts, 60 audio minutes, 30 video minutes,
101
  50 launch kits. Studio: $48.99 a month ($36.99 a month billed annually): 10 sources, 5,000 posts, 240 audio
102
  minutes, 120 video minutes, unlimited launch kits. Annual billing saves 24%.
 
96
  SEO pack, carousel).
97
  - **Publishing:** a Launchpad calendar that auto posts to X, LinkedIn and Bluesky, plus a topic map of rising and
98
  dormant themes and evergreen resurfacing of old posts.
99
+ - **Pricing (USD):** Free: 1 source, latest 5 posts indexed, 3 audio minutes, 1 video minute, 2 launch kits a month.
100
  Writer: $24.99 a month ($18.99 a month billed annually): 3 sources, 500 posts, 60 audio minutes, 30 video minutes,
101
  50 launch kits. Studio: $48.99 a month ($36.99 a month billed annually): 10 sources, 5,000 posts, 240 audio
102
  minutes, 120 video minutes, unlimited launch kits. Annual billing saves 24%.
frontend/src/api/types.ts CHANGED
@@ -25,7 +25,7 @@ export type Source = {
25
  sync_error: string | null;
26
  last_synced_at: string | null;
27
  document_count: number;
28
- /** Indexed but past the plan's post limit: listed, unlocked by upgrading. */
29
  locked_count: number;
30
  is_imports: boolean;
31
  };
@@ -40,7 +40,7 @@ export type Doc = {
40
  published_at: string | null;
41
  words: number;
42
  evergreen_score: number | null;
43
- /** Indexed but past the plan's post limit: preview only until the workspace upgrades. */
44
  locked: boolean;
45
  };
46
 
 
25
  sync_error: string | null;
26
  last_synced_at: string | null;
27
  document_count: number;
28
+ /** Found but past the plan's post limit: listed, not indexed until the workspace upgrades. */
29
  locked_count: number;
30
  is_imports: boolean;
31
  };
 
40
  published_at: string | null;
41
  words: number;
42
  evergreen_score: number | null;
43
+ /** Found but past the plan's post limit: title and link only, not indexed until the workspace upgrades. */
44
  locked: boolean;
45
  };
46
 
frontend/src/components/PricingTiers.tsx CHANGED
@@ -26,7 +26,7 @@ const FALLBACK: Plan[] = [
26
  tagline: "For trying Notestack on your archive",
27
  price_monthly_usd: 0,
28
  features: [
29
- "1 source, whole archive indexed, latest 5 posts available",
30
  "Grounded research chat with citations",
31
  "3 min of audio overviews a month",
32
  "1 min of video a month",
 
26
  tagline: "For trying Notestack on your archive",
27
  price_monthly_usd: 0,
28
  features: [
29
+ "1 source, your latest 5 posts indexed",
30
  "Grounded research chat with citations",
31
  "3 min of audio overviews a month",
32
  "1 min of video a month",
frontend/src/components/Reader.tsx CHANGED
@@ -74,14 +74,14 @@ export function Reader({
74
  {doc.preview && (
75
  <div className="welcome-cap locked-cap">
76
  <div>
77
- <strong>This post is indexed but locked</strong>
78
  <p className="muted small">
79
- Your plan makes your latest {doc.limit ?? 5} posts available. Upgrade to read it in full and use it in
80
- notebooks, audio, video and Launch Kits.
81
  </p>
82
  </div>
83
  <button type="button" className="btn btn-small btn-primary" onClick={() => openUpgrade("indexed_posts")}>
84
- Upgrade to unlock
85
  </button>
86
  </div>
87
  )}
 
74
  {doc.preview && (
75
  <div className="welcome-cap locked-cap">
76
  <div>
77
+ <strong>This post is not indexed</strong>
78
  <p className="muted small">
79
+ Your plan indexes your latest {doc.limit ?? 5} posts. Upgrade to index this one and the rest of your
80
+ archive, so you can ask about it and turn it into audio, video and Launch Kits.
81
  </p>
82
  </div>
83
  <button type="button" className="btn btn-small btn-primary" onClick={() => openUpgrade("indexed_posts")}>
84
+ Upgrade to index it
85
  </button>
86
  </div>
87
  )}
frontend/src/pages/Sources.tsx CHANGED
@@ -162,7 +162,7 @@ export default function Sources() {
162
  <strong>{s.title ?? s.feed_url}</strong>
163
  <span className="mono muted">
164
  {s.is_imports ? "imports" : s.platform} 路 {s.document_count} posts
165
- {s.locked_count > 0 ? ` (${s.document_count - s.locked_count} available, ${s.locked_count} locked)` : ""}
166
  {s.last_synced_at ? ` 路 synced ${formatDate(s.last_synced_at, true)}` : ""}
167
  </span>
168
  </div>
@@ -177,11 +177,14 @@ export default function Sources() {
177
  {s.locked_count > 0 && !active && (
178
  <div className="welcome-cap locked-cap">
179
  <div>
180
- <strong>{s.locked_count} posts indexed and locked</strong>
181
- <p className="muted small">Your plan makes your latest posts available. Upgrade to unlock the whole archive.</p>
 
 
 
182
  </div>
183
  <button type="button" className="btn btn-small btn-primary" onClick={() => openUpgrade("indexed_posts")}>
184
- Upgrade to unlock
185
  </button>
186
  </div>
187
  )}
@@ -237,8 +240,8 @@ export default function Sources() {
237
  {d.title}
238
  </button>
239
  {d.locked && (
240
- <span className="mono muted small" title="Indexed. Upgrade to use this post">
241
- Locked
242
  </span>
243
  )}
244
  <span className="mono muted">
 
162
  <strong>{s.title ?? s.feed_url}</strong>
163
  <span className="mono muted">
164
  {s.is_imports ? "imports" : s.platform} 路 {s.document_count} posts
165
+ {s.locked_count > 0 ? ` found, ${s.document_count - s.locked_count} indexed` : ""}
166
  {s.last_synced_at ? ` 路 synced ${formatDate(s.last_synced_at, true)}` : ""}
167
  </span>
168
  </div>
 
177
  {s.locked_count > 0 && !active && (
178
  <div className="welcome-cap locked-cap">
179
  <div>
180
+ <strong>{s.locked_count} posts not indexed</strong>
181
+ <p className="muted small">
182
+ Your plan only indexes your latest posts. Upgrade to index the rest so answers, audio and Launch Kits
183
+ can use your whole archive.
184
+ </p>
185
  </div>
186
  <button type="button" className="btn btn-small btn-primary" onClick={() => openUpgrade("indexed_posts")}>
187
+ Index all {s.document_count}
188
  </button>
189
  </div>
190
  )}
 
240
  {d.title}
241
  </button>
242
  {d.locked && (
243
+ <span className="mono muted small" title="Upgrade to index this post">
244
+ Not indexed
245
  </span>
246
  )}
247
  <span className="mono muted">
frontend/src/pages/Welcome.tsx CHANGED
@@ -144,7 +144,7 @@ export default function Welcome() {
144
 
145
  const indexingDone = live?.status === "done";
146
  const indexingFailed = live?.status === "failed";
147
- // Every post is indexed; the ingest job reports how many the plan makes available and how many are locked.
148
  const locked = Number(live?.result?.locked ?? 0);
149
  const capped = indexingDone && locked > 0;
150
  const available = Number(live?.result?.available ?? posts);
@@ -153,11 +153,11 @@ export default function Welcome() {
153
  <div className="welcome-cap">
154
  <div>
155
  <strong>
156
- All {found} posts indexed. Your latest {available} are available on the free plan.
157
  </strong>
158
  <p className="muted small">
159
- Upgrade to unlock the other {locked}, so answers, audio and Launch Kits draw on your whole archive. Nothing to
160
- reconnect: they unlock the moment you upgrade.
161
  </p>
162
  </div>
163
  <button
@@ -168,7 +168,7 @@ export default function Welcome() {
168
  navigate("/app?upgrade=1", { replace: true });
169
  }}
170
  >
171
- Upgrade to unlock {locked}
172
  </button>
173
  </div>
174
  ) : null;
@@ -336,7 +336,7 @@ export default function Welcome() {
336
 
337
  {step === "goal" && (
338
  <section className="card welcome-card">
339
- <p className="eyebrow">{indexingDone ? (capped ? `${available} of ${found} posts available` : `${posts} posts indexed`) : "Still indexing in the background"}</p>
340
  <h1>What should we launch first?</h1>
341
  {lockedNotice}
342
  <div className="goal-grid">
 
144
 
145
  const indexingDone = live?.status === "done";
146
  const indexingFailed = live?.status === "failed";
147
+ // The ingest job lists the whole archive but indexes only the plan's limit; the rest waits behind an upgrade.
148
  const locked = Number(live?.result?.locked ?? 0);
149
  const capped = indexingDone && locked > 0;
150
  const available = Number(live?.result?.available ?? posts);
 
153
  <div className="welcome-cap">
154
  <div>
155
  <strong>
156
+ We found {found} posts. The free plan indexed only your latest {available}.
157
  </strong>
158
  <p className="muted small">
159
+ The other {locked} are sitting in your archive, unread. Until you upgrade, answers, audio overviews and Launch
160
+ Kits can only use {available} posts. Upgrade and all {found} are indexed automatically.
161
  </p>
162
  </div>
163
  <button
 
168
  navigate("/app?upgrade=1", { replace: true });
169
  }}
170
  >
171
+ Index all {found} posts
172
  </button>
173
  </div>
174
  ) : null;
 
336
 
337
  {step === "goal" && (
338
  <section className="card welcome-card">
339
+ <p className="eyebrow">{indexingDone ? (capped ? `${available} of ${found} posts indexed` : `${posts} posts indexed`) : "Still indexing in the background"}</p>
340
  <h1>What should we launch first?</h1>
341
  {lockedNotice}
342
  <div className="goal-grid">