create-spark-html-app 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-spark-html-app",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Scaffold a spark-html app — dev/build/preview on Bun",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,55 +2,71 @@
2
2
 
3
3
  A complete blog: public posts with SEO titles/meta, dynamic routes, an
4
4
  about page, and an auth-gated admin panel where the author manages posts —
5
- and a private todo list. All of it is a handful of `.html` files, one SQLite
6
- database, and **zero build steps**.
5
+ and a live-updating private todo list. All of it is a handful of `.html`
6
+ files and one SQLite database. **No build step. No setup script. No fetch
7
+ calls for the basics.**
7
8
 
8
9
  ## Run it
9
10
 
10
11
  ```bash
11
12
  bun install
12
- bun run dev # seeds dev.db, serves on :3000, live-reloads on every edit
13
+ bun run dev # creates + seeds the DB from the templates, serves on :3000
13
14
  ```
14
15
 
15
- Sign in at [/admin](http://localhost:3000/admin) with
16
- **me@spark-html.com** / **spark**.
16
+ Sign in at [/login](http://localhost:3000/login) with
17
+ **me@spark-html.com** / **spark** — a plain form; it works with JavaScript
18
+ disabled.
17
19
 
18
20
  ## What's demonstrated where
19
21
 
20
22
  | Feature | Where |
21
23
  |---|---|
24
+ | Layouts | `pages/_layout.html` — nav, styles, `app.js` and the `author` query, once for every page |
22
25
  | Filesystem routing | `pages/index.html` → `/`, `pages/about.html` → `/about` |
23
26
  | Dynamic routes | `pages/blog/[slug].html` → `/blog/:slug` — `:slug` binds into the SQL |
24
- | Per-page `<title>`/`<meta>` | literal tags at the top of each page, `{expr}`-interpolated (spark-html-head/ssr) |
25
- | Declarative data | `<spark-ssr>` blocks the query names feed the template variables |
27
+ | Named page data | `posts = SELECT …` — the variable is named in the block, no endpoint exposed |
28
+ | Real 404s | `<template else status="404">` on the missing-post branch |
29
+ | Guards | `/admin`: `<spark-ssr guard="session" redirect="/login" />` — one line |
30
+ | No-JS forms | `pages/login.html` and the logout button — plain forms, 303 back |
31
+ | Form validation | the login form's `required`/`type="email"` run server-side too |
32
+ | The template is the schema | `seed/*.json` + `<spark-ssr table="…" seed="…">` — tables created and seeded at startup; `bun run db` shows the diff |
33
+ | Live updates | `<spark-ssr table="todos" … live />` — open /admin in two tabs |
34
+ | Per-page `<title>`/`<meta>` | literal tags at the top of each page, `{expr}`-interpolated |
35
+ | SEO | og:title/og:description derive from the head; `/sitemap.xml` enumerates `/blog/:slug` from its query; `/robots.txt` honors the admin page's `noindex` |
26
36
  | Server → component props | `index.html` passes each whole `post` row into `post-card.html` |
27
- | Pure-UI components | `nav.html`, `post-card.html` — no `<script>`, props are the scope |
28
- | Client components | `login-form`, `post-editor`, `todo-list` — their `<script>` runs in the browser |
37
+ | Client components | `post-editor`, `todo-list` — their `<script>` runs in the browser |
29
38
  | Auth | `spark.json` `"auth"` → sessions, `POST /api/users?auth` login, hashed passwords |
30
39
  | Auth-scoped CRUD | `posts` and `todos` carry `user_id` → their APIs are session-scoped (401 anonymous) |
31
40
  | Draft privacy | the `[slug]` query: `published = 1 OR :session.id IS NOT NULL` — authors preview drafts |
32
41
  | Middleware | `middleware.html` disables public signups (single-author blog) |
33
42
  | Aggregates | `about.html`'s `COUNT(*)` serves an object → `{stats.n}` |
34
- | Custom error page | `404.html` |
35
43
  | Dark/light theme | spark-html-theme — `app.js` one-liner + `theme-toggle.html`; no-flash init is auto-inlined |
36
- | Fonts | spark.json `"fonts"` → spark-html-font tags in every `<head>` (preload, no-shift fallback) |
37
- | Images | `bun run build` runs spark-html-image over `dist/` — webp + srcset for every `<img>` |
44
+ | Fonts | spark.json `"fonts"` → spark-html-font tags in every `<head>` |
45
+ | Images | `bun run build` runs spark-html-image over `dist/` — webp + srcset |
38
46
 
39
47
  ## The mental model
40
48
 
49
+ - `pages/_layout.html` wraps every page in the folder; `<slot>` is the page.
50
+ Its `<spark-ssr>` vars are in scope everywhere it wraps.
41
51
  - A **page**'s plain `<script>` runs on the **server** (it's the escape
42
52
  hatch); `<script type="module">`/`src` scripts ship to the browser.
43
53
  - A **component**'s `<script>` runs in the **browser**. Components are
44
54
  otherwise pure UI: they render the props they're given.
45
- - `<spark-ssr>` declares data. A `table="…"` gives you scoped REST CRUD;
46
- a `GET /api/x SELECT …` line is both an endpoint and page data.
47
- - Everything hot-reloads pages, components, queries, middleware — no
48
- restart, the browser refreshes itself.
55
+ - `<spark-ssr>` declares data. `var = SELECT …` names page data;
56
+ `table="…"` gives you scoped REST CRUD (+ `seed`, `live`, `limit`,
57
+ `search`); `guard="…"` protects the page.
58
+ - Plain forms to `/api/*` work without JavaScript — success 303s back,
59
+ the markup's constraint attributes validate on the server.
60
+ - Everything hot-reloads — pages, layouts, components, queries, middleware
61
+ — and dev errors land on the page, not in a bare 500. Open
62
+ [/__spark/plan](http://localhost:3000/__spark/plan) to see the inferred
63
+ backend.
49
64
 
50
65
  ## Deploy
51
66
 
52
67
  ```bash
53
- bun run build # dist/ + compiled binary; images optimized if sharp installs
68
+ bun run build # dist/ + compiled binary; images optimized
69
+ bun spark-ssr build --docker # …plus a Dockerfile
54
70
  PORT=3000 ./dist/app
55
71
  ```
56
72
 
@@ -1,6 +1,8 @@
1
1
  <!-- The author's private todos. Same auto-CRUD pattern as posts — the
2
2
  user_id column scopes every row to the session, so nobody else can
3
- read or touch them (try /api/todos signed out: 401). -->
3
+ read or touch them (try /api/todos signed out: 401). The todos block
4
+ declares `live`, so writes ping /__spark/live: the two lines at the
5
+ bottom keep every open tab (or the author's phone) in sync. -->
4
6
  <section class="panel">
5
7
  <h2>Private todos</h2>
6
8
  <div class="row">
@@ -48,4 +50,7 @@
48
50
  }
49
51
 
50
52
  refresh();
53
+
54
+ const es = new EventSource('/__spark/live');
55
+ es.onmessage = (e) => { if (e.data === 'todos') refresh(); };
51
56
  </script>
@@ -4,7 +4,8 @@
4
4
  "version": "0.0.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "bun setup.js && bun spark-ssr",
7
+ "dev": "bun spark-ssr",
8
+ "db": "bun spark-ssr db",
8
9
  "start": "bun spark-ssr start",
9
10
  "build": "bun spark-ssr build"
10
11
  },
@@ -0,0 +1,14 @@
1
+ <!-- The layout: every page in pages/ renders inside it — <slot> is the page.
2
+ Its <spark-ssr> vars (author) are in scope for every page it wraps, and
3
+ its head tags lift into <head> too (a page's own tags win on conflict).
4
+ The three lines every page used to repeat live here, once. -->
5
+ <link rel="stylesheet" href="/style.css">
6
+ <script type="module" src="/app.js"></script>
7
+
8
+ <div import="/components/nav" blog="{author.name}"></div>
9
+
10
+ <slot></slot>
11
+
12
+ <spark-ssr>
13
+ author = SELECT id, name, email, bio FROM users LIMIT 1
14
+ </spark-ssr>
@@ -1,11 +1,7 @@
1
- <!-- /about — profile straight from the users table, plus an aggregate:
1
+ <!-- /about — {author} comes from the layout; this page adds one aggregate:
2
2
  a COUNT(*) query serves an object, so {stats.n} just works. -->
3
3
  <title>About {author.name}</title>
4
4
  <meta name="description" content="Who is {author.name} — {author.bio}">
5
- <link rel="stylesheet" href="/style.css">
6
- <script type="module" src="/app.js"></script>
7
-
8
- <div import="/components/nav" blog="{author.name}" active="about"></div>
9
5
 
10
6
  <article class="post">
11
7
  <img class="avatar" src="/img/avatar.png" alt="{author.name}" width="88" height="88">
@@ -17,6 +13,5 @@
17
13
  </article>
18
14
 
19
15
  <spark-ssr>
20
- GET /api/author SELECT id, name, email, bio FROM users LIMIT 1
21
- GET /api/stats → SELECT COUNT(*) AS n FROM posts WHERE published = 1
16
+ stats = SELECT COUNT(*) AS n FROM posts WHERE published = 1
22
17
  </spark-ssr>
@@ -1,30 +1,25 @@
1
- <!-- /admin — the author's private side. `session` is always in page scope:
2
- signed out you get the login form, signed in the management panels.
3
- The two table declarations give the components their scoped REST APIs
4
- (posts + todos both carry user_id session-scoped, 401 for anyone else). -->
1
+ <!-- /admin — one guard line protects the whole page: signed out → 303 to
2
+ /login. The table blocks give the components their scoped REST APIs
3
+ (posts + todos carry user_id session-scoped, 401 for anyone else);
4
+ the seed="…" files define AND fill the tables on first run there is
5
+ no setup.js — and `live` keeps every open todos tab in sync. -->
5
6
  <title>Manage · {author.name}</title>
6
7
  <meta name="robots" content="noindex">
7
- <link rel="stylesheet" href="/style.css">
8
- <script type="module" src="/app.js"></script>
9
8
 
10
- <div import="/components/nav" blog="{author.name}" active="admin"></div>
9
+ <spark-ssr guard="session" redirect="/login" />
11
10
 
12
- <template if="session">
13
- <main class="admin">
14
- <header class="admin-head">
15
- <p>Signed in as <strong>{session.email}</strong></p>
16
- <div import="/components/logout-button"></div>
17
- </header>
18
- <div import="/components/post-editor"></div>
19
- <div import="/components/todo-list"></div>
20
- </main>
21
- </template>
22
- <template else>
23
- <div import="/components/login-form"></div>
24
- </template>
11
+ <main class="admin">
12
+ <header class="admin-head">
13
+ <p>Signed in as <strong>{session.email}</strong></p>
14
+ <!-- Logout is a plain form too — works with JavaScript disabled. -->
15
+ <form action="/api/logout" method="post" redirect="/">
16
+ <button class="ghost">Sign out</button>
17
+ </form>
18
+ </header>
19
+ <div import="/components/post-editor"></div>
20
+ <div import="/components/todo-list"></div>
21
+ </main>
25
22
 
26
- <spark-ssr table="posts" />
27
- <spark-ssr table="todos" />
28
- <spark-ssr>
29
- GET /api/author → SELECT id, name, email, bio FROM users LIMIT 1
30
- </spark-ssr>
23
+ <spark-ssr table="users" seed="./seed/users.json" />
24
+ <spark-ssr table="posts" seed="./seed/posts.json" />
25
+ <spark-ssr table="todos" seed="./seed/todos.json" live />
@@ -1,12 +1,9 @@
1
1
  <!-- /blog/:slug — the filename IS the route; :slug binds into the query.
2
- Drafts stay invisible to anonymous readers, but the signed-in author
2
+ A missing row is a REAL 404 (the status= on the rendered branch), and
3
+ drafts stay invisible to anonymous readers while the signed-in author
3
4
  gets a live preview (the OR :session.id clause). -->
4
5
  <title>{post ? post.title : 'Not found'} · {author.name}</title>
5
6
  <meta name="description" content="{post ? post.excerpt : 'This post does not exist.'}">
6
- <link rel="stylesheet" href="/style.css">
7
- <script type="module" src="/app.js"></script>
8
-
9
- <div import="/components/nav" blog="{author.name}"></div>
10
7
 
11
8
  <template if="post">
12
9
  <article class="post">
@@ -19,7 +16,7 @@
19
16
  <div class="body">{post.body}</div>
20
17
  </article>
21
18
  </template>
22
- <template else>
19
+ <template else status="404">
23
20
  <article class="post">
24
21
  <h1>Not found</h1>
25
22
  <p>That post doesn't exist — or it's a draft and you're not signed in.</p>
@@ -28,8 +25,7 @@
28
25
  </template>
29
26
 
30
27
  <spark-ssr>
31
- GET /api/author SELECT id, name, email, bio FROM users LIMIT 1
32
- GET /api/blog/[slug] → SELECT * FROM posts
28
+ post = SELECT * FROM posts
33
29
  WHERE slug = :slug AND (published = 1 OR :session.id IS NOT NULL)
34
30
  LIMIT 1
35
31
  </spark-ssr>
@@ -1,12 +1,8 @@
1
- <!-- The homepage. Everything the page needs is declared at the bottom in
2
- <spark-ssr>; the title/meta lift into <head> (spark-html-head/ssr);
3
- app.js is a client module (theme toggle). -->
1
+ <!-- The homepage. The layout owns the chrome and provides {author}; this
2
+ page declares only what's unique to it. `posts = SELECT …` is page data
3
+ with NO public endpoint name the variable, skip the route. -->
4
4
  <title>{author.name} — a spark blog</title>
5
5
  <meta name="description" content="{author.bio}">
6
- <link rel="stylesheet" href="/style.css">
7
- <script type="module" src="/app.js"></script>
8
-
9
- <div import="/components/nav" blog="{author.name}" active="home"></div>
10
6
 
11
7
  <header class="hero">
12
8
  <img class="avatar" src="/img/avatar.png" alt="{author.name}" width="88" height="88">
@@ -28,6 +24,5 @@
28
24
  </main>
29
25
 
30
26
  <spark-ssr>
31
- GET /api/author SELECT id, name, email, bio FROM users LIMIT 1
32
- GET /api/published → SELECT * FROM posts WHERE published = 1 ORDER BY created_at DESC
27
+ posts = SELECT * FROM posts WHERE published = 1 ORDER BY created_at DESC
33
28
  </spark-ssr>
@@ -0,0 +1,19 @@
1
+ <!-- Sign in with a PLAIN FORM — no JavaScript required. The server answers
2
+ a browser like a browser: success 303s to redirect="/admin"; a wrong
3
+ password re-renders this page with {errors} in scope. The `required`
4
+ attributes are enforced server-side too — the form is the validator. -->
5
+ <title>Sign in · {author.name}</title>
6
+ <meta name="robots" content="noindex">
7
+
8
+ <form class="panel login" action="/api/users?auth" method="post" redirect="/admin">
9
+ <h2>Author sign-in</h2>
10
+ <label>Email
11
+ <input type="email" name="email" autocomplete="username" required>
12
+ </label>
13
+ <label>Password
14
+ <input type="password" name="password" autocomplete="current-password" required>
15
+ </label>
16
+ <button>Sign in</button>
17
+ <template if="errors"><p class="error">Wrong email or password.</p></template>
18
+ <p class="hint">Seeded account: me@spark-html.com / spark</p>
19
+ </form>
@@ -0,0 +1,38 @@
1
+ [
2
+ {
3
+ "user_id": 1,
4
+ "slug": "hello-spark",
5
+ "title": "Hello, Spark",
6
+ "excerpt": "Why this blog runs on HTML that infers its own backend.",
7
+ "body": "This site is a handful of .html files and one SQLite database.\n\nEach page declares the data it needs in a <spark-ssr> block, and the server infers the rest: routes from filenames, APIs from SQL, auth from a user_id column — even these tables and rows came from seed files the templates point at.\n\nView source on any page — what you see is what I wrote.",
8
+ "published": 1,
9
+ "created_at": "2026-06-21"
10
+ },
11
+ {
12
+ "user_id": 1,
13
+ "slug": "html-that-reacts",
14
+ "title": "HTML that reacts",
15
+ "excerpt": "Templates, loops and conditionals — in plain HTML attributes.",
16
+ "body": "A <template each=\"post in posts\"> renders a list. A <template if> branches. {curly} braces interpolate.\n\nOn the server they render to static HTML; in the browser the same syntax comes alive as components. One mental model, both sides of the wire.",
17
+ "published": 1,
18
+ "created_at": "2026-06-27"
19
+ },
20
+ {
21
+ "user_id": 1,
22
+ "slug": "zero-config-ssr",
23
+ "title": "Zero-config SSR",
24
+ "excerpt": "Filesystem routing, layouts, sessions, live updates — from the template.",
25
+ "body": "pages/blog/[slug].html serves /blog/:slug, and :slug binds straight into the page query. pages/_layout.html wraps every page — nav, styles, footer, once.\n\nDeclare <spark-ssr table=\"todos\" live /> and the REST endpoints exist, the table is created from the template, and every open tab stays in sync. Delete the file and it all goes away.",
26
+ "published": 1,
27
+ "created_at": "2026-07-02"
28
+ },
29
+ {
30
+ "user_id": 1,
31
+ "slug": "drafts-are-private",
32
+ "title": "Drafts are private",
33
+ "excerpt": "This post is unpublished — only the signed-in author sees it.",
34
+ "body": "The page query keeps drafts out of anonymous requests:\n\nWHERE slug = :slug AND (published = 1 OR :session.id IS NOT NULL)\n\nAnd a missing row is a REAL 404 now — <template else status=\"404\"> sets the response status, so crawlers never index an empty page. Sign in at /admin and this post appears.",
35
+ "published": 0,
36
+ "created_at": "2026-07-04"
37
+ }
38
+ ]
@@ -0,0 +1,5 @@
1
+ [
2
+ { "user_id": 1, "title": "Finish the drafts post", "done": 1 },
3
+ { "user_id": 1, "title": "Write about companion packages", "done": 0 },
4
+ { "user_id": 1, "title": "Reply to reader mail", "done": 0 }
5
+ ]
@@ -0,0 +1,8 @@
1
+ [
2
+ {
3
+ "email": "me@spark-html.com",
4
+ "password": "spark",
5
+ "name": "Ada Spark",
6
+ "bio": "I write HTML that reacts. No compiler, no virtual DOM, no build step — just the platform, with a spark."
7
+ }
8
+ ]
@@ -1,38 +0,0 @@
1
- <!-- A component <script> runs in the BROWSER (pages' plain <script> runs on
2
- the server — that's the split). POST /api/users?auth is the login
3
- endpoint spark-ssr derives from the auth config. -->
4
- <form class="panel login" onsubmit={login}>
5
- <h2>Author sign-in</h2>
6
- <label>Email
7
- <input type="email" bind:value="email" autocomplete="username" required>
8
- </label>
9
- <label>Password
10
- <input type="password" bind:value="password" autocomplete="current-password" required>
11
- </label>
12
- <button :disabled="pending">{pending ? 'Signing in…' : 'Sign in'}</button>
13
- <template if="error"><p class="error">{error}</p></template>
14
- <p class="hint">Seeded account: me@spark-html.com / spark</p>
15
- </form>
16
-
17
- <script>
18
- let email = '';
19
- let password = '';
20
- let error = '';
21
- let pending = false;
22
-
23
- async function login() {
24
- pending = true;
25
- error = '';
26
- const res = await fetch('/api/users?auth', {
27
- method: 'POST',
28
- headers: { 'content-type': 'application/json' },
29
- body: JSON.stringify({ email, password }),
30
- });
31
- if (res.ok) {
32
- location.reload();
33
- } else {
34
- error = 'Wrong email or password.';
35
- pending = false;
36
- }
37
- }
38
- </script>
@@ -1,8 +0,0 @@
1
- <button class="ghost" onclick={logout}>Sign out</button>
2
-
3
- <script>
4
- async function logout() {
5
- await fetch('/api/logout', { method: 'POST' });
6
- location.href = '/';
7
- }
8
- </script>
@@ -1,76 +0,0 @@
1
- // One-time (idempotent) dev database setup — `bun run dev` runs it for you.
2
- // Swap spark.json's db to postgres:// any time; no code changes needed.
3
- import { Database } from 'bun:sqlite';
4
-
5
- const db = new Database('./dev.db', { create: true });
6
-
7
- db.run(`CREATE TABLE IF NOT EXISTS users (
8
- id INTEGER PRIMARY KEY AUTOINCREMENT,
9
- email TEXT NOT NULL UNIQUE,
10
- password TEXT NOT NULL,
11
- name TEXT NOT NULL,
12
- bio TEXT DEFAULT ''
13
- )`);
14
-
15
- db.run(`CREATE TABLE IF NOT EXISTS posts (
16
- id INTEGER PRIMARY KEY AUTOINCREMENT,
17
- user_id INTEGER NOT NULL,
18
- slug TEXT NOT NULL UNIQUE,
19
- title TEXT NOT NULL,
20
- excerpt TEXT DEFAULT '',
21
- body TEXT DEFAULT '',
22
- published INTEGER DEFAULT 0,
23
- created_at TEXT DEFAULT (date('now'))
24
- )`);
25
-
26
- db.run(`CREATE TABLE IF NOT EXISTS todos (
27
- id INTEGER PRIMARY KEY AUTOINCREMENT,
28
- user_id INTEGER NOT NULL,
29
- title TEXT NOT NULL,
30
- done INTEGER DEFAULT 0
31
- )`);
32
-
33
- if (db.query('SELECT COUNT(*) AS n FROM users').get().n === 0) {
34
- // The author account — sign in at /admin to manage the blog.
35
- const hash = await Bun.password.hash('spark');
36
- db.run('INSERT INTO users (email, password, name, bio) VALUES (?, ?, ?, ?)', [
37
- 'me@spark-html.com',
38
- hash,
39
- 'Ada Spark',
40
- 'I write HTML that reacts. No compiler, no virtual DOM, no build step — just the platform, with a spark.',
41
- ]);
42
-
43
- const posts = [
44
- ['hello-spark', 'Hello, Spark',
45
- 'Why this blog runs on HTML that infers its own backend.',
46
- 'This site is a handful of .html files and one SQLite database.\n\nEach page declares the data it needs in a <spark-ssr> block, and the server infers the rest: routes from filenames, APIs from SQL, auth from a user_id column.\n\nView source on any page — what you see is what I wrote.',
47
- 1, '2026-06-21'],
48
- ['html-that-reacts', 'HTML that reacts',
49
- 'Templates, loops and conditionals — in plain HTML attributes.',
50
- 'A <template each="post in posts"> renders a list. A <template if> branches. {curly} braces interpolate.\n\nOn the server they render to static HTML; in the browser the same syntax comes alive as components. One mental model, both sides of the wire.',
51
- 1, '2026-06-27'],
52
- ['zero-config-ssr', 'Zero-config SSR',
53
- 'Filesystem routing, sessions, uploads and CRUD — from the template.',
54
- 'pages/blog/[slug].html serves /blog/:slug, and :slug binds straight into the page query.\n\nDeclare <spark-ssr table="todos"> and the REST endpoints exist. Add a user_id column and they are scoped to the signed-in user. Delete the file and it all goes away.',
55
- 1, '2026-07-02'],
56
- ['drafts-are-private', 'Drafts are private',
57
- 'This post is unpublished — only the signed-in author sees it.',
58
- 'The page query keeps drafts out of anonymous requests:\n\nWHERE slug = :slug AND (published = 1 OR :session.id IS NOT NULL)\n\nSign in at /admin and this post appears on the homepage preview and here. Publish it from the admin panel when it is ready.',
59
- 0, '2026-07-04'],
60
- ];
61
- for (const [slug, title, excerpt, body, published, at] of posts) {
62
- db.run(
63
- 'INSERT INTO posts (user_id, slug, title, excerpt, body, published, created_at) VALUES (1, ?, ?, ?, ?, ?, ?)',
64
- [slug, title, excerpt, body, published, at],
65
- );
66
- }
67
-
68
- db.run(`INSERT INTO todos (user_id, title, done) VALUES
69
- (1, 'Finish the drafts post', 1),
70
- (1, 'Write about companion packages', 0),
71
- (1, 'Reply to reader mail', 0)`);
72
-
73
- console.log('⚡ seeded dev.db — sign in at /admin with me@spark-html.com / spark');
74
- }
75
-
76
- db.close();