nandi/gleanpublic Fork
⑂ b205012
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

Simplify application structure and remove obsolete code

nandithebull committed 2026-09-21T23:07:11-07:00 Browse files
b205012 parent: bc6fa20
modified app/README.md +1 -1
@@ -1,4 +1,4 @@
1-# glean_app
1+# pulseboard_app
22
33 A new Flutter project.
44
@@ -1,4 +1,4 @@
1-# glean_app1+# pulseboard_app
2 2
3 A new Flutter project.3 A new Flutter project.
4 4
modified app/android/app/build.gradle.kts +2 -2
@@ -5,7 +5,7 @@ plugins {
55 }
66
77 android {
8- namespace = "fr.rbrt.glean.glean_app"
8+ namespace = "fr.rbrt.pulseboard.pulseboard_app"
99 compileSdk = flutter.compileSdkVersion
1010 ndkVersion = flutter.ndkVersion
1111
@@ -16,7 +16,7 @@ android {
1616
1717 defaultConfig {
1818 // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
19- applicationId = "fr.rbrt.glean.glean_app"
19+ applicationId = "fr.rbrt.pulseboard.pulseboard_app"
2020 // You can update the following values to match your application needs.
2121 // For more information, see: https://flutter.dev/to/review-gradle-config.
2222 minSdk = flutter.minSdkVersion
@@ -5,7 +5,7 @@ plugins {
5 }5 }
6 6
7 android {7 android {
8- namespace = "fr.rbrt.glean.glean_app"8+ namespace = "fr.rbrt.pulseboard.pulseboard_app"
9 compileSdk = flutter.compileSdkVersion9 compileSdk = flutter.compileSdkVersion
10 ndkVersion = flutter.ndkVersion10 ndkVersion = flutter.ndkVersion
11 11
@@ -16,7 +16,7 @@ android {
16 16
17 defaultConfig {17 defaultConfig {
18 // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).18 // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
19- applicationId = "fr.rbrt.glean.glean_app"19+ applicationId = "fr.rbrt.pulseboard.pulseboard_app"
20 // You can update the following values to match your application needs.20 // You can update the following values to match your application needs.
21 // For more information, see: https://flutter.dev/to/review-gradle-config.21 // For more information, see: https://flutter.dev/to/review-gradle-config.
22 minSdk = flutter.minSdkVersion22 minSdk = flutter.minSdkVersion
modified app/android/app/src/main/AndroidManifest.xml +1 -1
@@ -1,6 +1,6 @@
11 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
22 <application
3- android:label="glean_app"
3+ android:label="pulseboard_app"
44 android:name="${applicationName}"
55 android:icon="@mipmap/ic_launcher">
66 <activity
@@ -1,6 +1,6 @@
1 <manifest xmlns:android="http://schemas.android.com/apk/res/android">1 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2 <application2 <application
3- android:label="glean_app"3+ android:label="pulseboard_app"
4 android:name="${applicationName}"4 android:name="${applicationName}"
5 android:icon="@mipmap/ic_launcher">5 android:icon="@mipmap/ic_launcher">
6 <activity6 <activity
renamed app/android/app/src/main/kotlin/fr/rbrt/pulseboard/pulseboard_app/MainActivity.kt +1 -1
similarity index 68%
rename from app/android/app/src/main/kotlin/fr/rbrt/glean/glean_app/MainActivity.kt
rename to app/android/app/src/main/kotlin/fr/rbrt/pulseboard/pulseboard_app/MainActivity.kt
@@ -1,4 +1,4 @@
1-package fr.rbrt.glean.glean_app
1+package fr.rbrt.pulseboard.pulseboard_app
22
33 import io.flutter.embedding.android.FlutterActivity
44
similarity index 68%
rename from app/android/app/src/main/kotlin/fr/rbrt/glean/glean_app/MainActivity.kt
rename to app/android/app/src/main/kotlin/fr/rbrt/pulseboard/pulseboard_app/MainActivity.kt
@@ -1,4 +1,4 @@
1-package fr.rbrt.glean.glean_app1+package fr.rbrt.pulseboard.pulseboard_app
2 2
3 import io.flutter.embedding.android.FlutterActivity3 import io.flutter.embedding.android.FlutterActivity
4 4
modified app/lib/main.dart +2 -2
@@ -38,8 +38,8 @@ class _PulseboardAppState extends State<PulseboardApp> {
3838 child: MaterialApp(
3939 title: 'Pulseboard',
4040 debugShowCheckedModeBanner: false,
41- theme: gleanTheme(Brightness.light),
42- darkTheme: gleanTheme(Brightness.dark),
41+ theme: pulseboardTheme(Brightness.light),
42+ darkTheme: pulseboardTheme(Brightness.dark),
4343 home: const HomeShell(),
4444 ),
4545 );
@@ -38,8 +38,8 @@ class _PulseboardAppState extends State<PulseboardApp> {
38 child: MaterialApp(38 child: MaterialApp(
39 title: 'Pulseboard',39 title: 'Pulseboard',
40 debugShowCheckedModeBanner: false,40 debugShowCheckedModeBanner: false,
41- theme: gleanTheme(Brightness.light),41+ theme: pulseboardTheme(Brightness.light),
42- darkTheme: gleanTheme(Brightness.dark),42+ darkTheme: pulseboardTheme(Brightness.dark),
43 home: const HomeShell(),43 home: const HomeShell(),
44 ),44 ),
45 );45 );
modified app/lib/src/api/client.dart +4 -4
@@ -16,13 +16,13 @@ class ApiException implements Exception {
1616 ///
1717 /// No cookies, no CSRF, no auth: the server is single-user and binds
1818 /// loopback. That is why this class is a thin wrapper rather than a session.
19-class GleanClient {
20- GleanClient({required this.baseUrl, this.token = '', http.Client? client})
19+class PulseboardClient {
20+ PulseboardClient({required this.baseUrl, this.token = '', http.Client? client})
2121 : _client = client ?? http.Client();
2222
2323 final String baseUrl;
2424
25- /// Shared secret, when the reader is running with GLEAN_TOKEN set. Empty
25+ /// Shared secret, when the reader is running with PULSEBOARD_TOKEN set. Empty
2626 /// for a local reader, which has no gate.
2727 ///
2828 /// Sent as a header rather than a query parameter so it stays out of the
@@ -32,7 +32,7 @@ class GleanClient {
3232 final http.Client _client;
3333
3434 Map<String, String> get _headers =>
35- token.isEmpty ? const {} : {'X-Glean-Token': token};
35+ token.isEmpty ? const {} : {'X-Pulseboard-Token': token};
3636
3737 Uri _uri(String path, [Map<String, String>? query]) {
3838 final q = query?.entries.where((e) => e.value.isNotEmpty);
@@ -16,13 +16,13 @@ class ApiException implements Exception {
16 ///16 ///
17 /// No cookies, no CSRF, no auth: the server is single-user and binds17 /// No cookies, no CSRF, no auth: the server is single-user and binds
18 /// loopback. That is why this class is a thin wrapper rather than a session.18 /// loopback. That is why this class is a thin wrapper rather than a session.
19-class GleanClient {19+class PulseboardClient {
20- GleanClient({required this.baseUrl, this.token = '', http.Client? client})20+ PulseboardClient({required this.baseUrl, this.token = '', http.Client? client})
21 : _client = client ?? http.Client();21 : _client = client ?? http.Client();
22 22
23 final String baseUrl;23 final String baseUrl;
24 24
25- /// Shared secret, when the reader is running with GLEAN_TOKEN set. Empty25+ /// Shared secret, when the reader is running with PULSEBOARD_TOKEN set. Empty
26 /// for a local reader, which has no gate.26 /// for a local reader, which has no gate.
27 ///27 ///
28 /// Sent as a header rather than a query parameter so it stays out of the28 /// Sent as a header rather than a query parameter so it stays out of the
@@ -32,7 +32,7 @@ class GleanClient {
32 final http.Client _client;32 final http.Client _client;
33 33
34 Map<String, String> get _headers =>34 Map<String, String> get _headers =>
35- token.isEmpty ? const {} : {'X-Glean-Token': token};35+ token.isEmpty ? const {} : {'X-Pulseboard-Token': token};
36 36
37 Uri _uri(String path, [Map<String, String>? query]) {37 Uri _uri(String path, [Map<String, String>? query]) {
38 final q = query?.entries.where((e) => e.value.isNotEmpty);38 final q = query?.entries.where((e) => e.value.isNotEmpty);
modified app/lib/src/app_state.dart +7 -7
@@ -5,7 +5,7 @@ import 'api/client.dart';
55 import 'models/models.dart';
66
77 /// Where the reader is. Override at build time:
8-/// flutter run --dart-define=GLEAN_BASE_URL=http://10.0.2.2:8080
8+/// flutter run --dart-define=PULSEBOARD_BASE_URL=http://10.0.2.2:8080
99 ///
1010 /// 10.0.2.2 is how the Android emulator reaches the host's loopback; a real
1111 /// device needs the machine's LAN address.
@@ -14,7 +14,7 @@ import 'models/models.dart';
1414 /// relative paths: same origin, which means no CORS preflight and the
1515 /// reader's own cookie on every request.
1616 const kConfiguredBaseUrl = String.fromEnvironment(
17- 'GLEAN_BASE_URL',
17+ 'PULSEBOARD_BASE_URL',
1818 defaultValue: '',
1919 );
2020
@@ -22,12 +22,12 @@ String get kDefaultBaseUrl => kConfiguredBaseUrl.isNotEmpty
2222 ? kConfiguredBaseUrl
2323 : (kIsWeb ? '' : 'http://127.0.0.1:8080');
2424
25-/// Shared secret, for a reader running with GLEAN_TOKEN set.
25+/// Shared secret, for a reader running with PULSEBOARD_TOKEN set.
2626 ///
2727 /// Empty by default, which is right for a local reader and for the web build
2828 /// served by the reader itself -- there the browser already holds the cookie
2929 /// the reader set, and same-origin requests carry it automatically.
30-const kToken = String.fromEnvironment('GLEAN_TOKEN', defaultValue: '');
30+const kToken = String.fromEnvironment('PULSEBOARD_TOKEN', defaultValue: '');
3131
3232 /// Feed list and unread counts, shared across screens.
3333 ///
@@ -38,10 +38,10 @@ class AppState extends ChangeNotifier {
3838 AppState({
3939 String? baseUrl,
4040 String token = kToken,
41- GleanClient? client,
42- }) : client = client ?? GleanClient(baseUrl: baseUrl ?? kDefaultBaseUrl, token: token);
41+ PulseboardClient? client,
42+ }) : client = client ?? PulseboardClient(baseUrl: baseUrl ?? kDefaultBaseUrl, token: token);
4343
44- final GleanClient client;
44+ final PulseboardClient client;
4545
4646 List<Feed> _feeds = const [];
4747 int _unread = 0;
@@ -5,7 +5,7 @@ import 'api/client.dart';
5 import 'models/models.dart';5 import 'models/models.dart';
6 6
7 /// Where the reader is. Override at build time:7 /// Where the reader is. Override at build time:
8-/// flutter run --dart-define=GLEAN_BASE_URL=http://10.0.2.2:80808+/// flutter run --dart-define=PULSEBOARD_BASE_URL=http://10.0.2.2:8080
9 ///9 ///
10 /// 10.0.2.2 is how the Android emulator reaches the host's loopback; a real10 /// 10.0.2.2 is how the Android emulator reaches the host's loopback; a real
11 /// device needs the machine's LAN address.11 /// device needs the machine's LAN address.
@@ -14,7 +14,7 @@ import 'models/models.dart';
14 /// relative paths: same origin, which means no CORS preflight and the14 /// relative paths: same origin, which means no CORS preflight and the
15 /// reader's own cookie on every request.15 /// reader's own cookie on every request.
16 const kConfiguredBaseUrl = String.fromEnvironment(16 const kConfiguredBaseUrl = String.fromEnvironment(
17- 'GLEAN_BASE_URL',17+ 'PULSEBOARD_BASE_URL',
18 defaultValue: '',18 defaultValue: '',
19 );19 );
20 20
@@ -22,12 +22,12 @@ String get kDefaultBaseUrl => kConfiguredBaseUrl.isNotEmpty
22 ? kConfiguredBaseUrl22 ? kConfiguredBaseUrl
23 : (kIsWeb ? '' : 'http://127.0.0.1:8080');23 : (kIsWeb ? '' : 'http://127.0.0.1:8080');
24 24
25-/// Shared secret, for a reader running with GLEAN_TOKEN set.25+/// Shared secret, for a reader running with PULSEBOARD_TOKEN set.
26 ///26 ///
27 /// Empty by default, which is right for a local reader and for the web build27 /// Empty by default, which is right for a local reader and for the web build
28 /// served by the reader itself -- there the browser already holds the cookie28 /// served by the reader itself -- there the browser already holds the cookie
29 /// the reader set, and same-origin requests carry it automatically.29 /// the reader set, and same-origin requests carry it automatically.
30-const kToken = String.fromEnvironment('GLEAN_TOKEN', defaultValue: '');30+const kToken = String.fromEnvironment('PULSEBOARD_TOKEN', defaultValue: '');
31 31
32 /// Feed list and unread counts, shared across screens.32 /// Feed list and unread counts, shared across screens.
33 ///33 ///
@@ -38,10 +38,10 @@ class AppState extends ChangeNotifier {
38 AppState({38 AppState({
39 String? baseUrl,39 String? baseUrl,
40 String token = kToken,40 String token = kToken,
41- GleanClient? client,41+ PulseboardClient? client,
42- }) : client = client ?? GleanClient(baseUrl: baseUrl ?? kDefaultBaseUrl, token: token);42+ }) : client = client ?? PulseboardClient(baseUrl: baseUrl ?? kDefaultBaseUrl, token: token);
43 43
44- final GleanClient client;44+ final PulseboardClient client;
45 45
46 List<Feed> _feeds = const [];46 List<Feed> _feeds = const [];
47 int _unread = 0;47 int _unread = 0;
modified app/lib/src/screens/articles_screen.dart +1 -1
@@ -172,7 +172,7 @@ class _StatusFilter extends StatelessWidget {
172172
173173 @override
174174 Widget build(BuildContext context) {
175- final c = GleanColors.of(context);
175+ final c = PulseboardColors.of(context);
176176 return DropdownButton<String>(
177177 value: status,
178178 underline: const SizedBox.shrink(),
@@ -172,7 +172,7 @@ class _StatusFilter extends StatelessWidget {
172 172
173 @override173 @override
174 Widget build(BuildContext context) {174 Widget build(BuildContext context) {
175- final c = GleanColors.of(context);175+ final c = PulseboardColors.of(context);
176 return DropdownButton<String>(176 return DropdownButton<String>(
177 value: status,177 value: status,
178 underline: const SizedBox.shrink(),178 underline: const SizedBox.shrink(),
modified app/lib/src/screens/feed_manager_screen.dart +1 -1
@@ -108,7 +108,7 @@ class _FeedManagerScreenState extends State<FeedManagerScreen> {
108108 @override
109109 Widget build(BuildContext context) {
110110 final app = AppScope.of(context);
111- final c = GleanColors.of(context);
111+ final c = PulseboardColors.of(context);
112112 final filter = _filter.text.trim().toLowerCase();
113113 final feeds = app.feeds.where((feed) {
114114 return filter.isEmpty ||
@@ -108,7 +108,7 @@ class _FeedManagerScreenState extends State<FeedManagerScreen> {
108 @override108 @override
109 Widget build(BuildContext context) {109 Widget build(BuildContext context) {
110 final app = AppScope.of(context);110 final app = AppScope.of(context);
111- final c = GleanColors.of(context);111+ final c = PulseboardColors.of(context);
112 final filter = _filter.text.trim().toLowerCase();112 final filter = _filter.text.trim().toLowerCase();
113 final feeds = app.feeds.where((feed) {113 final feeds = app.feeds.where((feed) {
114 return filter.isEmpty ||114 return filter.isEmpty ||
modified app/lib/src/screens/home_shell.dart +3 -3
@@ -96,11 +96,11 @@ class _HomeShellState extends State<HomeShell> {
9696 return FeedManagerScreen(onClose: _closeManageFeeds);
9797 }
9898 final app = AppScope.of(context);
99- final c = GleanColors.of(context);
99+ final c = PulseboardColors.of(context);
100100
101101 return Scaffold(
102102 appBar: AppBar(
103- title: Text(_selected.isAll ? 'glean' : _selected.title),
103+ title: Text(_selected.isAll ? 'Pulseboard' : _selected.title),
104104 actions: [
105105 IconButton(
106106 tooltip: 'Manage feeds',
@@ -170,7 +170,7 @@ class _HomeShellState extends State<HomeShell> {
170170 overflow: TextOverflow.ellipsis,
171171 ),
172172 trailing: f.unread > 0
173- ? GleanTag('${f.unread}', emphasis: !f.isAll)
173+ ? PulseboardTag('${f.unread}', emphasis: !f.isAll)
174174 : null,
175175 onTap: () {
176176 setState(() => _selected = f);
@@ -96,11 +96,11 @@ class _HomeShellState extends State<HomeShell> {
96 return FeedManagerScreen(onClose: _closeManageFeeds);96 return FeedManagerScreen(onClose: _closeManageFeeds);
97 }97 }
98 final app = AppScope.of(context);98 final app = AppScope.of(context);
99- final c = GleanColors.of(context);99+ final c = PulseboardColors.of(context);
100 100
101 return Scaffold(101 return Scaffold(
102 appBar: AppBar(102 appBar: AppBar(
103- title: Text(_selected.isAll ? 'glean' : _selected.title),103+ title: Text(_selected.isAll ? 'Pulseboard' : _selected.title),
104 actions: [104 actions: [
105 IconButton(105 IconButton(
106 tooltip: 'Manage feeds',106 tooltip: 'Manage feeds',
@@ -170,7 +170,7 @@ class _HomeShellState extends State<HomeShell> {
170 overflow: TextOverflow.ellipsis,170 overflow: TextOverflow.ellipsis,
171 ),171 ),
172 trailing: f.unread > 0172 trailing: f.unread > 0
173- ? GleanTag('${f.unread}', emphasis: !f.isAll)173+ ? PulseboardTag('${f.unread}', emphasis: !f.isAll)
174 : null,174 : null,
175 onTap: () {175 onTap: () {
176 setState(() => _selected = f);176 setState(() => _selected = f);
modified app/lib/src/theme.dart +12 -12
@@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
22
33 /// Flutter port of web/src/app.css.
44 ///
5-/// Glean's web design system is deliberately brutalist: a monochrome ink/paper
5+/// Pulseboard's web design system is deliberately brutalist: a monochrome ink/paper
66 /// base with one accent green, sharp corners, 2px borders and hard offset
77 /// shadows, monospace throughout. Keeping that identity matters more than
88 /// looking like stock Material, so this leans on a shared token set rather
99 /// than ColorScheme.fromSeed.
10-class GleanColors extends ThemeExtension<GleanColors> {
11- const GleanColors({
10+class PulseboardColors extends ThemeExtension<PulseboardColors> {
11+ const PulseboardColors({
1212 required this.bg,
1313 required this.fg,
1414 required this.surface,
@@ -30,7 +30,7 @@ class GleanColors extends ThemeExtension<GleanColors> {
3030 final Color accentInk;
3131 final Color danger;
3232
33- static const light = GleanColors(
33+ static const light = PulseboardColors(
3434 bg: Color(0xFFFAFAF7),
3535 fg: Color(0xFF0A0A0A),
3636 surface: Color(0xFFF0EFE9),
@@ -42,7 +42,7 @@ class GleanColors extends ThemeExtension<GleanColors> {
4242 danger: Color(0xFFC82014),
4343 );
4444
45- static const dark = GleanColors(
45+ static const dark = PulseboardColors(
4646 bg: Color(0xFF0A0A0A),
4747 fg: Color(0xFFF5F5EF),
4848 surface: Color(0xFF161616),
@@ -54,15 +54,15 @@ class GleanColors extends ThemeExtension<GleanColors> {
5454 danger: Color(0xFFFF5A4D),
5555 );
5656
57- static GleanColors of(BuildContext context) =>
58- Theme.of(context).extension<GleanColors>()!;
57+ static PulseboardColors of(BuildContext context) =>
58+ Theme.of(context).extension<PulseboardColors>()!;
5959
6060 @override
61- GleanColors copyWith() => this;
61+ PulseboardColors copyWith() => this;
6262
6363 @override
64- GleanColors lerp(ThemeExtension<GleanColors>? other, double t) =>
65- t < 0.5 ? this : (other as GleanColors? ?? this);
64+ PulseboardColors lerp(ThemeExtension<PulseboardColors>? other, double t) =>
65+ t < 0.5 ? this : (other as PulseboardColors? ?? this);
6666 }
6767
6868 /// The web app asks for JetBrains Mono and falls back through IBM Plex Mono to
@@ -70,8 +70,8 @@ class GleanColors extends ThemeExtension<GleanColors> {
7070 /// runtime font download; bundling the TTF would pin it exactly.
7171 const kMonoFallback = <String>['JetBrains Mono', 'IBM Plex Mono', 'monospace'];
7272
73-ThemeData gleanTheme(Brightness brightness) {
74- final c = brightness == Brightness.dark ? GleanColors.dark : GleanColors.light;
73+ThemeData pulseboardTheme(Brightness brightness) {
74+ final c = brightness == Brightness.dark ? PulseboardColors.dark : PulseboardColors.light;
7575
7676 TextStyle mono(double size, {FontWeight weight = FontWeight.w400, Color? color}) =>
7777 TextStyle(
@@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
2 2
3 /// Flutter port of web/src/app.css.3 /// Flutter port of web/src/app.css.
4 ///4 ///
5-/// Glean's web design system is deliberately brutalist: a monochrome ink/paper5+/// Pulseboard's web design system is deliberately brutalist: a monochrome ink/paper
6 /// base with one accent green, sharp corners, 2px borders and hard offset6 /// base with one accent green, sharp corners, 2px borders and hard offset
7 /// shadows, monospace throughout. Keeping that identity matters more than7 /// shadows, monospace throughout. Keeping that identity matters more than
8 /// looking like stock Material, so this leans on a shared token set rather8 /// looking like stock Material, so this leans on a shared token set rather
9 /// than ColorScheme.fromSeed.9 /// than ColorScheme.fromSeed.
10-class GleanColors extends ThemeExtension<GleanColors> {10+class PulseboardColors extends ThemeExtension<PulseboardColors> {
11- const GleanColors({11+ const PulseboardColors({
12 required this.bg,12 required this.bg,
13 required this.fg,13 required this.fg,
14 required this.surface,14 required this.surface,
@@ -30,7 +30,7 @@ class GleanColors extends ThemeExtension<GleanColors> {
30 final Color accentInk;30 final Color accentInk;
31 final Color danger;31 final Color danger;
32 32
33- static const light = GleanColors(33+ static const light = PulseboardColors(
34 bg: Color(0xFFFAFAF7),34 bg: Color(0xFFFAFAF7),
35 fg: Color(0xFF0A0A0A),35 fg: Color(0xFF0A0A0A),
36 surface: Color(0xFFF0EFE9),36 surface: Color(0xFFF0EFE9),
@@ -42,7 +42,7 @@ class GleanColors extends ThemeExtension<GleanColors> {
42 danger: Color(0xFFC82014),42 danger: Color(0xFFC82014),
43 );43 );
44 44
45- static const dark = GleanColors(45+ static const dark = PulseboardColors(
46 bg: Color(0xFF0A0A0A),46 bg: Color(0xFF0A0A0A),
47 fg: Color(0xFFF5F5EF),47 fg: Color(0xFFF5F5EF),
48 surface: Color(0xFF161616),48 surface: Color(0xFF161616),
@@ -54,15 +54,15 @@ class GleanColors extends ThemeExtension<GleanColors> {
54 danger: Color(0xFFFF5A4D),54 danger: Color(0xFFFF5A4D),
55 );55 );
56 56
57- static GleanColors of(BuildContext context) =>57+ static PulseboardColors of(BuildContext context) =>
58- Theme.of(context).extension<GleanColors>()!;58+ Theme.of(context).extension<PulseboardColors>()!;
59 59
60 @override60 @override
61- GleanColors copyWith() => this;61+ PulseboardColors copyWith() => this;
62 62
63 @override63 @override
64- GleanColors lerp(ThemeExtension<GleanColors>? other, double t) =>64+ PulseboardColors lerp(ThemeExtension<PulseboardColors>? other, double t) =>
65- t < 0.5 ? this : (other as GleanColors? ?? this);65+ t < 0.5 ? this : (other as PulseboardColors? ?? this);
66 }66 }
67 67
68 /// The web app asks for JetBrains Mono and falls back through IBM Plex Mono to68 /// The web app asks for JetBrains Mono and falls back through IBM Plex Mono to
@@ -70,8 +70,8 @@ class GleanColors extends ThemeExtension<GleanColors> {
70 /// runtime font download; bundling the TTF would pin it exactly.70 /// runtime font download; bundling the TTF would pin it exactly.
71 const kMonoFallback = <String>['JetBrains Mono', 'IBM Plex Mono', 'monospace'];71 const kMonoFallback = <String>['JetBrains Mono', 'IBM Plex Mono', 'monospace'];
72 72
73-ThemeData gleanTheme(Brightness brightness) {73+ThemeData pulseboardTheme(Brightness brightness) {
74- final c = brightness == Brightness.dark ? GleanColors.dark : GleanColors.light;74+ final c = brightness == Brightness.dark ? PulseboardColors.dark : PulseboardColors.light;
75 75
76 TextStyle mono(double size, {FontWeight weight = FontWeight.w400, Color? color}) =>76 TextStyle mono(double size, {FontWeight weight = FontWeight.w400, Color? color}) =>
77 TextStyle(77 TextStyle(
modified app/lib/src/widgets/article_tile.dart +1 -1
@@ -20,7 +20,7 @@ class ArticleTile extends StatelessWidget {
2020
2121 @override
2222 Widget build(BuildContext context) {
23- final c = GleanColors.of(context);
23+ final c = PulseboardColors.of(context);
2424 final text = Theme.of(context).textTheme;
2525 final read = article.isRead;
2626
@@ -20,7 +20,7 @@ class ArticleTile extends StatelessWidget {
20 20
21 @override21 @override
22 Widget build(BuildContext context) {22 Widget build(BuildContext context) {
23- final c = GleanColors.of(context);23+ final c = PulseboardColors.of(context);
24 final text = Theme.of(context).textTheme;24 final text = Theme.of(context).textTheme;
25 final read = article.isRead;25 final read = article.isRead;
26 26
modified app/lib/src/widgets/common.dart +13 -13
@@ -5,8 +5,8 @@ import '../theme.dart';
55
66 /// A hard-edged box with a 2px border and the offset shadow the web app uses
77 /// for cards and buttons.
8-class GleanBox extends StatelessWidget {
9- const GleanBox({
8+class PulseboardBox extends StatelessWidget {
9+ const PulseboardBox({
1010 super.key,
1111 required this.child,
1212 this.padding = const EdgeInsets.all(12),
@@ -23,7 +23,7 @@ class GleanBox extends StatelessWidget {
2323
2424 @override
2525 Widget build(BuildContext context) {
26- final c = GleanColors.of(context);
26+ final c = PulseboardColors.of(context);
2727 final box = Container(
2828 padding: padding,
2929 decoration: BoxDecoration(
@@ -41,15 +41,15 @@ class GleanBox extends StatelessWidget {
4141 }
4242
4343 /// Monospace pill used for counts, categories and other metadata.
44-class GleanTag extends StatelessWidget {
45- const GleanTag(this.label, {super.key, this.emphasis = false});
44+class PulseboardTag extends StatelessWidget {
45+ const PulseboardTag(this.label, {super.key, this.emphasis = false});
4646
4747 final String label;
4848 final bool emphasis;
4949
5050 @override
5151 Widget build(BuildContext context) {
52- final c = GleanColors.of(context);
52+ final c = PulseboardColors.of(context);
5353 return Container(
5454 padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
5555 decoration: BoxDecoration(
@@ -68,8 +68,8 @@ class GleanTag extends StatelessWidget {
6868 }
6969
7070 /// Square-cornered primary button.
71-class GleanButton extends StatelessWidget {
72- const GleanButton({
71+class PulseboardButton extends StatelessWidget {
72+ const PulseboardButton({
7373 super.key,
7474 required this.label,
7575 this.onPressed,
@@ -84,11 +84,11 @@ class GleanButton extends StatelessWidget {
8484
8585 @override
8686 Widget build(BuildContext context) {
87- final c = GleanColors.of(context);
87+ final c = PulseboardColors.of(context);
8888 final enabled = onPressed != null && !busy;
8989 return Opacity(
9090 opacity: enabled ? 1 : 0.5,
91- child: GleanBox(
91+ child: PulseboardBox(
9292 onTap: enabled ? onPressed : null,
9393 filled: !accent,
9494 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
@@ -171,7 +171,7 @@ class ErrorView extends StatelessWidget {
171171
172172 @override
173173 Widget build(BuildContext context) {
174- final c = GleanColors.of(context);
174+ final c = PulseboardColors.of(context);
175175 return Center(
176176 child: Padding(
177177 padding: const EdgeInsets.all(24),
@@ -187,7 +187,7 @@ class ErrorView extends StatelessWidget {
187187 const SizedBox(height: 8),
188188 Text(message, textAlign: TextAlign.center),
189189 const SizedBox(height: 16),
190- GleanButton(label: 'Retry', onPressed: onRetry),
190+ PulseboardButton(label: 'Retry', onPressed: onRetry),
191191 ],
192192 ),
193193 ),
@@ -226,7 +226,7 @@ class FaviconBadge extends StatelessWidget {
226226
227227 @override
228228 Widget build(BuildContext context) {
229- final c = GleanColors.of(context);
229+ final c = PulseboardColors.of(context);
230230 final letter = seed.isEmpty ? '?' : seed.characters.first.toUpperCase();
231231 final fallback = Container(
232232 width: size,
@@ -5,8 +5,8 @@ import '../theme.dart';
5 5
6 /// A hard-edged box with a 2px border and the offset shadow the web app uses6 /// A hard-edged box with a 2px border and the offset shadow the web app uses
7 /// for cards and buttons.7 /// for cards and buttons.
8-class GleanBox extends StatelessWidget {8+class PulseboardBox extends StatelessWidget {
9- const GleanBox({9+ const PulseboardBox({
10 super.key,10 super.key,
11 required this.child,11 required this.child,
12 this.padding = const EdgeInsets.all(12),12 this.padding = const EdgeInsets.all(12),
@@ -23,7 +23,7 @@ class GleanBox extends StatelessWidget {
23 23
24 @override24 @override
25 Widget build(BuildContext context) {25 Widget build(BuildContext context) {
26- final c = GleanColors.of(context);26+ final c = PulseboardColors.of(context);
27 final box = Container(27 final box = Container(
28 padding: padding,28 padding: padding,
29 decoration: BoxDecoration(29 decoration: BoxDecoration(
@@ -41,15 +41,15 @@ class GleanBox extends StatelessWidget {
41 }41 }
42 42
43 /// Monospace pill used for counts, categories and other metadata.43 /// Monospace pill used for counts, categories and other metadata.
44-class GleanTag extends StatelessWidget {44+class PulseboardTag extends StatelessWidget {
45- const GleanTag(this.label, {super.key, this.emphasis = false});45+ const PulseboardTag(this.label, {super.key, this.emphasis = false});
46 46
47 final String label;47 final String label;
48 final bool emphasis;48 final bool emphasis;
49 49
50 @override50 @override
51 Widget build(BuildContext context) {51 Widget build(BuildContext context) {
52- final c = GleanColors.of(context);52+ final c = PulseboardColors.of(context);
53 return Container(53 return Container(
54 padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),54 padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
55 decoration: BoxDecoration(55 decoration: BoxDecoration(
@@ -68,8 +68,8 @@ class GleanTag extends StatelessWidget {
68 }68 }
69 69
70 /// Square-cornered primary button.70 /// Square-cornered primary button.
71-class GleanButton extends StatelessWidget {71+class PulseboardButton extends StatelessWidget {
72- const GleanButton({72+ const PulseboardButton({
73 super.key,73 super.key,
74 required this.label,74 required this.label,
75 this.onPressed,75 this.onPressed,
@@ -84,11 +84,11 @@ class GleanButton extends StatelessWidget {
84 84
85 @override85 @override
86 Widget build(BuildContext context) {86 Widget build(BuildContext context) {
87- final c = GleanColors.of(context);87+ final c = PulseboardColors.of(context);
88 final enabled = onPressed != null && !busy;88 final enabled = onPressed != null && !busy;
89 return Opacity(89 return Opacity(
90 opacity: enabled ? 1 : 0.5,90 opacity: enabled ? 1 : 0.5,
91- child: GleanBox(91+ child: PulseboardBox(
92 onTap: enabled ? onPressed : null,92 onTap: enabled ? onPressed : null,
93 filled: !accent,93 filled: !accent,
94 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),94 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
@@ -171,7 +171,7 @@ class ErrorView extends StatelessWidget {
171 171
172 @override172 @override
173 Widget build(BuildContext context) {173 Widget build(BuildContext context) {
174- final c = GleanColors.of(context);174+ final c = PulseboardColors.of(context);
175 return Center(175 return Center(
176 child: Padding(176 child: Padding(
177 padding: const EdgeInsets.all(24),177 padding: const EdgeInsets.all(24),
@@ -187,7 +187,7 @@ class ErrorView extends StatelessWidget {
187 const SizedBox(height: 8),187 const SizedBox(height: 8),
188 Text(message, textAlign: TextAlign.center),188 Text(message, textAlign: TextAlign.center),
189 const SizedBox(height: 16),189 const SizedBox(height: 16),
190- GleanButton(label: 'Retry', onPressed: onRetry),190+ PulseboardButton(label: 'Retry', onPressed: onRetry),
191 ],191 ],
192 ),192 ),
193 ),193 ),
@@ -226,7 +226,7 @@ class FaviconBadge extends StatelessWidget {
226 226
227 @override227 @override
228 Widget build(BuildContext context) {228 Widget build(BuildContext context) {
229- final c = GleanColors.of(context);229+ final c = PulseboardColors.of(context);
230 final letter = seed.isEmpty ? '?' : seed.characters.first.toUpperCase();230 final letter = seed.isEmpty ? '?' : seed.characters.first.toUpperCase();
231 final fallback = Container(231 final fallback = Container(
232 width: size,232 width: size,
modified app/linux/CMakeLists.txt +2 -2
@@ -4,10 +4,10 @@ project(runner LANGUAGES CXX)
44
55 # The name of the executable created for the application. Change this to change
66 # the on-disk name of your application.
7-set(BINARY_NAME "glean_app")
7+set(BINARY_NAME "pulseboard_app")
88 # The unique GTK application identifier for this application. See:
99 # https://wiki.gnome.org/HowDoI/ChooseApplicationID
10-set(APPLICATION_ID "fr.rbrt.glean.glean_app")
10+set(APPLICATION_ID "fr.rbrt.pulseboard.pulseboard_app")
1111
1212 # Explicitly opt in to modern CMake behaviors to avoid warnings with recent
1313 # versions of CMake.
@@ -4,10 +4,10 @@ project(runner LANGUAGES CXX)
4 4
5 # The name of the executable created for the application. Change this to change5 # The name of the executable created for the application. Change this to change
6 # the on-disk name of your application.6 # the on-disk name of your application.
7-set(BINARY_NAME "glean_app")7+set(BINARY_NAME "pulseboard_app")
8 # The unique GTK application identifier for this application. See:8 # The unique GTK application identifier for this application. See:
9 # https://wiki.gnome.org/HowDoI/ChooseApplicationID9 # https://wiki.gnome.org/HowDoI/ChooseApplicationID
10-set(APPLICATION_ID "fr.rbrt.glean.glean_app")10+set(APPLICATION_ID "fr.rbrt.pulseboard.pulseboard_app")
11 11
12 # Explicitly opt in to modern CMake behaviors to avoid warnings with recent12 # Explicitly opt in to modern CMake behaviors to avoid warnings with recent
13 # versions of CMake.13 # versions of CMake.
modified app/linux/runner/my_application.cc +2 -2
@@ -45,11 +45,11 @@ static void my_application_activate(GApplication* application) {
4545 if (use_header_bar) {
4646 GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
4747 gtk_widget_show(GTK_WIDGET(header_bar));
48- gtk_header_bar_set_title(header_bar, "glean_app");
48+ gtk_header_bar_set_title(header_bar, "pulseboard_app");
4949 gtk_header_bar_set_show_close_button(header_bar, TRUE);
5050 gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
5151 } else {
52- gtk_window_set_title(window, "glean_app");
52+ gtk_window_set_title(window, "pulseboard_app");
5353 }
5454
5555 gtk_window_set_default_size(window, 1280, 720);
@@ -45,11 +45,11 @@ static void my_application_activate(GApplication* application) {
45 if (use_header_bar) {45 if (use_header_bar) {
46 GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());46 GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
47 gtk_widget_show(GTK_WIDGET(header_bar));47 gtk_widget_show(GTK_WIDGET(header_bar));
48- gtk_header_bar_set_title(header_bar, "glean_app");48+ gtk_header_bar_set_title(header_bar, "pulseboard_app");
49 gtk_header_bar_set_show_close_button(header_bar, TRUE);49 gtk_header_bar_set_show_close_button(header_bar, TRUE);
50 gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));50 gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
51 } else {51 } else {
52- gtk_window_set_title(window, "glean_app");52+ gtk_window_set_title(window, "pulseboard_app");
53 }53 }
54 54
55 gtk_window_set_default_size(window, 1280, 720);55 gtk_window_set_default_size(window, 1280, 720);
modified app/pubspec.yaml +1 -1
@@ -1,4 +1,4 @@
1-name: glean_app
1+name: pulseboard_app
22 description: "A new Flutter project."
33 # The following line prevents the package from being accidentally published to
44 # pub.dev using `flutter pub publish`. This is preferred for private packages.
@@ -1,4 +1,4 @@
1-name: glean_app1+name: pulseboard_app
2 description: "A new Flutter project."2 description: "A new Flutter project."
3 # The following line prevents the package from being accidentally published to3 # The following line prevents the package from being accidentally published to
4 # pub.dev using `flutter pub publish`. This is preferred for private packages.4 # pub.dev using `flutter pub publish`. This is preferred for private packages.
modified app/test/live_reader_test.dart +5 -5
@@ -4,18 +4,18 @@ library;
44 import 'dart:io';
55
66 import 'package:flutter_test/flutter_test.dart';
7-import 'package:glean_app/src/api/client.dart';
7+import 'package:pulseboard_app/src/api/client.dart';
88
99 /// Checks the client against a running reader, which is the only thing that
1010 /// proves the two agree on field names. A mock returns whatever the test
1111 /// author believed the server sends.
1212 ///
13-/// flutter test --tags live --dart-define=GLEAN_BASE_URL=http://127.0.0.1:8080
13+/// flutter test --tags live --dart-define=PULSEBOARD_BASE_URL=http://127.0.0.1:8080
1414 void main() {
15- const base = String.fromEnvironment('GLEAN_BASE_URL',
15+ const base = String.fromEnvironment('PULSEBOARD_BASE_URL',
1616 defaultValue: 'http://127.0.0.1:8080');
1717
18- late GleanClient client;
18+ late PulseboardClient client;
1919
2020 setUpAll(() {
2121 TestWidgetsFlutterBinding.ensureInitialized();
@@ -24,7 +24,7 @@ void main() {
2424 HttpOverrides.global = null;
2525 });
2626
27- setUp(() => client = GleanClient(baseUrl: base));
27+ setUp(() => client = PulseboardClient(baseUrl: base));
2828 tearDown(() => client.close());
2929
3030 test('feeds and unread parse', () async {
@@ -4,18 +4,18 @@ library;
4 import 'dart:io';4 import 'dart:io';
5 5
6 import 'package:flutter_test/flutter_test.dart';6 import 'package:flutter_test/flutter_test.dart';
7-import 'package:glean_app/src/api/client.dart';7+import 'package:pulseboard_app/src/api/client.dart';
8 8
9 /// Checks the client against a running reader, which is the only thing that9 /// Checks the client against a running reader, which is the only thing that
10 /// proves the two agree on field names. A mock returns whatever the test10 /// proves the two agree on field names. A mock returns whatever the test
11 /// author believed the server sends.11 /// author believed the server sends.
12 ///12 ///
13-/// flutter test --tags live --dart-define=GLEAN_BASE_URL=http://127.0.0.1:808013+/// flutter test --tags live --dart-define=PULSEBOARD_BASE_URL=http://127.0.0.1:8080
14 void main() {14 void main() {
15- const base = String.fromEnvironment('GLEAN_BASE_URL',15+ const base = String.fromEnvironment('PULSEBOARD_BASE_URL',
16 defaultValue: 'http://127.0.0.1:8080');16 defaultValue: 'http://127.0.0.1:8080');
17 17
18- late GleanClient client;18+ late PulseboardClient client;
19 19
20 setUpAll(() {20 setUpAll(() {
21 TestWidgetsFlutterBinding.ensureInitialized();21 TestWidgetsFlutterBinding.ensureInitialized();
@@ -24,7 +24,7 @@ void main() {
24 HttpOverrides.global = null;24 HttpOverrides.global = null;
25 });25 });
26 26
27- setUp(() => client = GleanClient(baseUrl: base));27+ setUp(() => client = PulseboardClient(baseUrl: base));
28 tearDown(() => client.close());28 tearDown(() => client.close());
29 29
30 test('feeds and unread parse', () async {30 test('feeds and unread parse', () async {
modified app/test/widget_test.dart +6 -6
@@ -5,10 +5,10 @@ import 'package:flutter_test/flutter_test.dart';
55 import 'package:http/http.dart' as http;
66 import 'package:http/testing.dart';
77
8-import 'package:glean_app/src/api/client.dart';
9-import 'package:glean_app/src/app_state.dart';
10-import 'package:glean_app/src/screens/home_shell.dart';
11-import 'package:glean_app/src/theme.dart';
8+import 'package:pulseboard_app/src/api/client.dart';
9+import 'package:pulseboard_app/src/app_state.dart';
10+import 'package:pulseboard_app/src/screens/home_shell.dart';
11+import 'package:pulseboard_app/src/theme.dart';
1212
1313 /// A stand-in for the reader, covering the routes the shell touches.
1414 http.Client fakeReader({List<String>? seen, bool empty = false}) {
@@ -66,14 +66,14 @@ http.Client fakeReader({List<String>? seen, bool empty = false}) {
6666 Future<AppState> pump(WidgetTester tester,
6767 {List<String>? seen, bool empty = false}) async {
6868 final state = AppState(
69- client: GleanClient(
69+ client: PulseboardClient(
7070 baseUrl: 'https://reader.test',
7171 client: fakeReader(seen: seen, empty: empty)),
7272 );
7373 await tester.pumpWidget(AppScope(
7474 state: state,
7575 child: MaterialApp(
76- theme: gleanTheme(Brightness.light),
76+ theme: pulseboardTheme(Brightness.light),
7777 home: const HomeShell(),
7878 ),
7979 ));
@@ -5,10 +5,10 @@ import 'package:flutter_test/flutter_test.dart';
5 import 'package:http/http.dart' as http;5 import 'package:http/http.dart' as http;
6 import 'package:http/testing.dart';6 import 'package:http/testing.dart';
7 7
8-import 'package:glean_app/src/api/client.dart';8+import 'package:pulseboard_app/src/api/client.dart';
9-import 'package:glean_app/src/app_state.dart';9+import 'package:pulseboard_app/src/app_state.dart';
10-import 'package:glean_app/src/screens/home_shell.dart';10+import 'package:pulseboard_app/src/screens/home_shell.dart';
11-import 'package:glean_app/src/theme.dart';11+import 'package:pulseboard_app/src/theme.dart';
12 12
13 /// A stand-in for the reader, covering the routes the shell touches.13 /// A stand-in for the reader, covering the routes the shell touches.
14 http.Client fakeReader({List<String>? seen, bool empty = false}) {14 http.Client fakeReader({List<String>? seen, bool empty = false}) {
@@ -66,14 +66,14 @@ http.Client fakeReader({List<String>? seen, bool empty = false}) {
66 Future<AppState> pump(WidgetTester tester,66 Future<AppState> pump(WidgetTester tester,
67 {List<String>? seen, bool empty = false}) async {67 {List<String>? seen, bool empty = false}) async {
68 final state = AppState(68 final state = AppState(
69- client: GleanClient(69+ client: PulseboardClient(
70 baseUrl: 'https://reader.test',70 baseUrl: 'https://reader.test',
71 client: fakeReader(seen: seen, empty: empty)),71 client: fakeReader(seen: seen, empty: empty)),
72 );72 );
73 await tester.pumpWidget(AppScope(73 await tester.pumpWidget(AppScope(
74 state: state,74 state: state,
75 child: MaterialApp(75 child: MaterialApp(
76- theme: gleanTheme(Brightness.light),76+ theme: pulseboardTheme(Brightness.light),
77 home: const HomeShell(),77 home: const HomeShell(),
78 ),78 ),
79 ));79 ));
modified deploy/modal_app.py +25 -25
@@ -29,14 +29,14 @@ import modal
2929
3030 REPO = pathlib.Path(__file__).parent.parent
3131
32-APP_NAME = "glean-reader"
32+APP_NAME = "pulseboard"
3333 PORT = 8080
34-PUBLIC_URL = "https://codegod100--glean-reader-serve.modal.run"
34+PUBLIC_URL = "https://codegod100--pulseboard-serve.modal.run"
3535
3636 VOLUME_PATH = "/data"
3737 SNAPSHOT_DIR = f"{VOLUME_PATH}/db"
3838 LIVE_DIR = "/livedb"
39-DB_BASE = f"{LIVE_DIR}/glean"
39+DB_BASE = f"{LIVE_DIR}/pulseboard"
4040
4141 # The reader opens <base>_users and attaches <base>_articles.
4242 DB_SUFFIXES = ("_users", "_articles")
@@ -57,28 +57,28 @@ image = (
5757 REPO / "spike" / "nim",
5858 remote_path="/src",
5959 copy=True,
60- ignore=["nimcache", "glean", "*_probe", "*.db*"],
60+ ignore=["nimcache", "pulseboard", "*_probe", "*.db*"],
6161 )
6262 # The Flutter web build is committed output rather than built here:
6363 # installing Flutter in the image would add gigabytes and minutes for
6464 # something the developer machine has already produced.
6565 .add_local_dir(REPO / "app" / "build" / "web", remote_path="/web", copy=True)
66- .run_commands("cd /src && nim c -d:release -d:ssl --hints:off -o:/usr/local/bin/glean glean.nim")
66+ .run_commands("cd /src && nim c -d:release -d:ssl --hints:off -o:/usr/local/bin/pulseboard pulseboard.nim")
6767 )
6868
69-volume = modal.Volume.from_name("glean-reader-data", create_if_missing=True)
69+volume = modal.Volume.from_name("pulseboard-data", create_if_missing=True)
7070
71-# GLEAN_TOKEN. Without it the reader binds loopback and Modal's proxy reaches
71+# PULSEBOARD_TOKEN. Without it the reader binds loopback and Modal's proxy reaches
7272 # nothing, so this is required rather than optional:
73-# modal secret create glean-reader GLEAN_TOKEN="$(openssl rand -hex 24)"
74-secret = modal.Secret.from_name("glean-reader")
73+# modal secret create pulseboard PULSEBOARD_TOKEN="$(openssl rand -hex 24)"
74+secret = modal.Secret.from_name("pulseboard")
7575
7676 app = modal.App(APP_NAME)
7777
7878
7979 @app.function(image=image, secrets=[secret], timeout=300)
8080 def import_opml(opml: str) -> str:
81- """Import a backup through the live API without revealing GLEAN_TOKEN.
81+ """Import a backup through the live API without revealing PULSEBOARD_TOKEN.
8282
8383 Run with ``modal run deploy/modal_app.py::import_opml --opml "$(<backup.opml)"``.
8484 Keeping this as a Modal function means only workspace members can invoke
@@ -87,11 +87,11 @@ def import_opml(opml: str) -> str:
8787 from urllib.parse import urlencode
8888 from urllib.request import Request, urlopen
8989
90- token = os.environ["GLEAN_TOKEN"]
90+ token = os.environ["PULSEBOARD_TOKEN"]
9191 request = Request(
9292 f"{PUBLIC_URL}/opml",
9393 data=urlencode({"opml": opml}).encode(),
94- headers={"X-Glean-Token": token},
94+ headers={"X-Pulseboard-Token": token},
9595 method="POST",
9696 )
9797 with urlopen(request, timeout=240) as response:
@@ -103,7 +103,7 @@ def inspect_feeds() -> str:
103103 """Return live feed and unread-article data for a recovery check."""
104104 from urllib.request import Request, urlopen
105105
106- headers = {"X-Glean-Token": os.environ["GLEAN_TOKEN"]}
106+ headers = {"X-Pulseboard-Token": os.environ["PULSEBOARD_TOKEN"]}
107107 results = {}
108108 for path in ("/feeds", "/articles?status=unread&limit=5"):
109109 with urlopen(Request(f"{PUBLIC_URL}{path}", headers=headers), timeout=50) as response:
@@ -132,7 +132,7 @@ def _snapshot_once() -> None:
132132 subprocess.run(
133133 ["sqlite3", live, f"VACUUM INTO '{staged}'"], check=True, capture_output=True
134134 )
135- shutil.copy2(staged, f"{SNAPSHOT_DIR}/glean{suffix}")
135+ shutil.copy2(staged, f"{SNAPSHOT_DIR}/pulseboard{suffix}")
136136 os.remove(staged)
137137 volume.commit()
138138
@@ -140,10 +140,10 @@ def _snapshot_once() -> None:
140140 def _restore() -> None:
141141 os.makedirs(LIVE_DIR, exist_ok=True)
142142 for suffix in DB_SUFFIXES:
143- snap = f"{SNAPSHOT_DIR}/glean{suffix}"
143+ snap = f"{SNAPSHOT_DIR}/pulseboard{suffix}"
144144 if os.path.exists(snap):
145145 shutil.copy2(snap, f"{DB_BASE}{suffix}")
146- print(f"[glean] restored {snap}", flush=True)
146+ print(f"[pulseboard] restored {snap}", flush=True)
147147
148148
149149 def _snapshot_loop(stop: threading.Event) -> None:
@@ -151,7 +151,7 @@ def _snapshot_loop(stop: threading.Event) -> None:
151151 try:
152152 _snapshot_once()
153153 except Exception as exc: # a failed snapshot must not kill the server
154- print(f"[glean] snapshot failed: {exc}", file=sys.stderr, flush=True)
154+ print(f"[pulseboard] snapshot failed: {exc}", file=sys.stderr, flush=True)
155155
156156
157157 @app.function(
@@ -170,16 +170,16 @@ def serve() -> None:
170170 _restore()
171171
172172 env = dict(os.environ)
173- env["GLEAN_DB"] = DB_BASE
174- env["GLEAN_PORT"] = str(PORT)
175- env["GLEAN_WEB"] = "/web"
176- if not env.get("GLEAN_TOKEN"):
173+ env["PULSEBOARD_DB"] = DB_BASE
174+ env["PULSEBOARD_PORT"] = str(PORT)
175+ env["PULSEBOARD_WEB"] = "/web"
176+ if not env.get("PULSEBOARD_TOKEN"):
177177 # The reader would bind loopback and Modal's proxy would reach
178178 # nothing, which looks like a broken deploy rather than a missing
179179 # secret. Fail with the reason instead.
180- raise RuntimeError("GLEAN_TOKEN is required: the reader has no other auth")
180+ raise RuntimeError("PULSEBOARD_TOKEN is required: the reader has no other auth")
181181
182- proc = subprocess.Popen(["/usr/local/bin/glean"], env=env)
182+ proc = subprocess.Popen(["/usr/local/bin/pulseboard"], env=env)
183183
184184 stop = threading.Event()
185185 threading.Thread(target=_snapshot_loop, args=(stop,), daemon=True).start()
@@ -190,9 +190,9 @@ def serve() -> None:
190190 try:
191191 time.sleep(1)
192192 _snapshot_once()
193- print("[glean] final snapshot written", flush=True)
193+ print("[pulseboard] final snapshot written", flush=True)
194194 except Exception as exc:
195- print(f"[glean] final snapshot failed: {exc}", file=sys.stderr, flush=True)
195+ print(f"[pulseboard] final snapshot failed: {exc}", file=sys.stderr, flush=True)
196196 sys.exit(0)
197197
198198 signal.signal(signal.SIGTERM, shutdown)
@@ -29,14 +29,14 @@ import modal
29 29
30 REPO = pathlib.Path(__file__).parent.parent30 REPO = pathlib.Path(__file__).parent.parent
31 31
32-APP_NAME = "glean-reader"32+APP_NAME = "pulseboard"
33 PORT = 808033 PORT = 8080
34-PUBLIC_URL = "https://codegod100--glean-reader-serve.modal.run"34+PUBLIC_URL = "https://codegod100--pulseboard-serve.modal.run"
35 35
36 VOLUME_PATH = "/data"36 VOLUME_PATH = "/data"
37 SNAPSHOT_DIR = f"{VOLUME_PATH}/db"37 SNAPSHOT_DIR = f"{VOLUME_PATH}/db"
38 LIVE_DIR = "/livedb"38 LIVE_DIR = "/livedb"
39-DB_BASE = f"{LIVE_DIR}/glean"39+DB_BASE = f"{LIVE_DIR}/pulseboard"
40 40
41 # The reader opens <base>_users and attaches <base>_articles.41 # The reader opens <base>_users and attaches <base>_articles.
42 DB_SUFFIXES = ("_users", "_articles")42 DB_SUFFIXES = ("_users", "_articles")
@@ -57,28 +57,28 @@ image = (
57 REPO / "spike" / "nim",57 REPO / "spike" / "nim",
58 remote_path="/src",58 remote_path="/src",
59 copy=True,59 copy=True,
60- ignore=["nimcache", "glean", "*_probe", "*.db*"],60+ ignore=["nimcache", "pulseboard", "*_probe", "*.db*"],
61 )61 )
62 # The Flutter web build is committed output rather than built here:62 # The Flutter web build is committed output rather than built here:
63 # installing Flutter in the image would add gigabytes and minutes for63 # installing Flutter in the image would add gigabytes and minutes for
64 # something the developer machine has already produced.64 # something the developer machine has already produced.
65 .add_local_dir(REPO / "app" / "build" / "web", remote_path="/web", copy=True)65 .add_local_dir(REPO / "app" / "build" / "web", remote_path="/web", copy=True)
66- .run_commands("cd /src && nim c -d:release -d:ssl --hints:off -o:/usr/local/bin/glean glean.nim")66+ .run_commands("cd /src && nim c -d:release -d:ssl --hints:off -o:/usr/local/bin/pulseboard pulseboard.nim")
67 )67 )
68 68
69-volume = modal.Volume.from_name("glean-reader-data", create_if_missing=True)69+volume = modal.Volume.from_name("pulseboard-data", create_if_missing=True)
70 70
71-# GLEAN_TOKEN. Without it the reader binds loopback and Modal's proxy reaches71+# PULSEBOARD_TOKEN. Without it the reader binds loopback and Modal's proxy reaches
72 # nothing, so this is required rather than optional:72 # nothing, so this is required rather than optional:
73-# modal secret create glean-reader GLEAN_TOKEN="$(openssl rand -hex 24)"73+# modal secret create pulseboard PULSEBOARD_TOKEN="$(openssl rand -hex 24)"
74-secret = modal.Secret.from_name("glean-reader")74+secret = modal.Secret.from_name("pulseboard")
75 75
76 app = modal.App(APP_NAME)76 app = modal.App(APP_NAME)
77 77
78 78
79 @app.function(image=image, secrets=[secret], timeout=300)79 @app.function(image=image, secrets=[secret], timeout=300)
80 def import_opml(opml: str) -> str:80 def import_opml(opml: str) -> str:
81- """Import a backup through the live API without revealing GLEAN_TOKEN.81+ """Import a backup through the live API without revealing PULSEBOARD_TOKEN.
82 82
83 Run with ``modal run deploy/modal_app.py::import_opml --opml "$(<backup.opml)"``.83 Run with ``modal run deploy/modal_app.py::import_opml --opml "$(<backup.opml)"``.
84 Keeping this as a Modal function means only workspace members can invoke84 Keeping this as a Modal function means only workspace members can invoke
@@ -87,11 +87,11 @@ def import_opml(opml: str) -> str:
87 from urllib.parse import urlencode87 from urllib.parse import urlencode
88 from urllib.request import Request, urlopen88 from urllib.request import Request, urlopen
89 89
90- token = os.environ["GLEAN_TOKEN"]90+ token = os.environ["PULSEBOARD_TOKEN"]
91 request = Request(91 request = Request(
92 f"{PUBLIC_URL}/opml",92 f"{PUBLIC_URL}/opml",
93 data=urlencode({"opml": opml}).encode(),93 data=urlencode({"opml": opml}).encode(),
94- headers={"X-Glean-Token": token},94+ headers={"X-Pulseboard-Token": token},
95 method="POST",95 method="POST",
96 )96 )
97 with urlopen(request, timeout=240) as response:97 with urlopen(request, timeout=240) as response:
@@ -103,7 +103,7 @@ def inspect_feeds() -> str:
103 """Return live feed and unread-article data for a recovery check."""103 """Return live feed and unread-article data for a recovery check."""
104 from urllib.request import Request, urlopen104 from urllib.request import Request, urlopen
105 105
106- headers = {"X-Glean-Token": os.environ["GLEAN_TOKEN"]}106+ headers = {"X-Pulseboard-Token": os.environ["PULSEBOARD_TOKEN"]}
107 results = {}107 results = {}
108 for path in ("/feeds", "/articles?status=unread&limit=5"):108 for path in ("/feeds", "/articles?status=unread&limit=5"):
109 with urlopen(Request(f"{PUBLIC_URL}{path}", headers=headers), timeout=50) as response:109 with urlopen(Request(f"{PUBLIC_URL}{path}", headers=headers), timeout=50) as response:
@@ -132,7 +132,7 @@ def _snapshot_once() -> None:
132 subprocess.run(132 subprocess.run(
133 ["sqlite3", live, f"VACUUM INTO '{staged}'"], check=True, capture_output=True133 ["sqlite3", live, f"VACUUM INTO '{staged}'"], check=True, capture_output=True
134 )134 )
135- shutil.copy2(staged, f"{SNAPSHOT_DIR}/glean{suffix}")135+ shutil.copy2(staged, f"{SNAPSHOT_DIR}/pulseboard{suffix}")
136 os.remove(staged)136 os.remove(staged)
137 volume.commit()137 volume.commit()
138 138
@@ -140,10 +140,10 @@ def _snapshot_once() -> None:
140 def _restore() -> None:140 def _restore() -> None:
141 os.makedirs(LIVE_DIR, exist_ok=True)141 os.makedirs(LIVE_DIR, exist_ok=True)
142 for suffix in DB_SUFFIXES:142 for suffix in DB_SUFFIXES:
143- snap = f"{SNAPSHOT_DIR}/glean{suffix}"143+ snap = f"{SNAPSHOT_DIR}/pulseboard{suffix}"
144 if os.path.exists(snap):144 if os.path.exists(snap):
145 shutil.copy2(snap, f"{DB_BASE}{suffix}")145 shutil.copy2(snap, f"{DB_BASE}{suffix}")
146- print(f"[glean] restored {snap}", flush=True)146+ print(f"[pulseboard] restored {snap}", flush=True)
147 147
148 148
149 def _snapshot_loop(stop: threading.Event) -> None:149 def _snapshot_loop(stop: threading.Event) -> None:
@@ -151,7 +151,7 @@ def _snapshot_loop(stop: threading.Event) -> None:
151 try:151 try:
152 _snapshot_once()152 _snapshot_once()
153 except Exception as exc: # a failed snapshot must not kill the server153 except Exception as exc: # a failed snapshot must not kill the server
154- print(f"[glean] snapshot failed: {exc}", file=sys.stderr, flush=True)154+ print(f"[pulseboard] snapshot failed: {exc}", file=sys.stderr, flush=True)
155 155
156 156
157 @app.function(157 @app.function(
@@ -170,16 +170,16 @@ def serve() -> None:
170 _restore()170 _restore()
171 171
172 env = dict(os.environ)172 env = dict(os.environ)
173- env["GLEAN_DB"] = DB_BASE173+ env["PULSEBOARD_DB"] = DB_BASE
174- env["GLEAN_PORT"] = str(PORT)174+ env["PULSEBOARD_PORT"] = str(PORT)
175- env["GLEAN_WEB"] = "/web"175+ env["PULSEBOARD_WEB"] = "/web"
176- if not env.get("GLEAN_TOKEN"):176+ if not env.get("PULSEBOARD_TOKEN"):
177 # The reader would bind loopback and Modal's proxy would reach177 # The reader would bind loopback and Modal's proxy would reach
178 # nothing, which looks like a broken deploy rather than a missing178 # nothing, which looks like a broken deploy rather than a missing
179 # secret. Fail with the reason instead.179 # secret. Fail with the reason instead.
180- raise RuntimeError("GLEAN_TOKEN is required: the reader has no other auth")180+ raise RuntimeError("PULSEBOARD_TOKEN is required: the reader has no other auth")
181 181
182- proc = subprocess.Popen(["/usr/local/bin/glean"], env=env)182+ proc = subprocess.Popen(["/usr/local/bin/pulseboard"], env=env)
183 183
184 stop = threading.Event()184 stop = threading.Event()
185 threading.Thread(target=_snapshot_loop, args=(stop,), daemon=True).start()185 threading.Thread(target=_snapshot_loop, args=(stop,), daemon=True).start()
@@ -190,9 +190,9 @@ def serve() -> None:
190 try:190 try:
191 time.sleep(1)191 time.sleep(1)
192 _snapshot_once()192 _snapshot_once()
193- print("[glean] final snapshot written", flush=True)193+ print("[pulseboard] final snapshot written", flush=True)
194 except Exception as exc:194 except Exception as exc:
195- print(f"[glean] final snapshot failed: {exc}", file=sys.stderr, flush=True)195+ print(f"[pulseboard] final snapshot failed: {exc}", file=sys.stderr, flush=True)
196 sys.exit(0)196 sys.exit(0)
197 197
198 signal.signal(signal.SIGTERM, shutdown)198 signal.signal(signal.SIGTERM, shutdown)
modified deploy/readme.md +3 -3
@@ -1,8 +1,8 @@
11 # Deploying
22
33 ```
4-modal secret create glean-reader GLEAN_TOKEN="$(openssl rand -hex 24)"
5-cd app && flutter build web --release --dart-define=GLEAN_BASE_URL=
4+modal secret create pulseboard PULSEBOARD_TOKEN="$(openssl rand -hex 24)"
5+cd app && flutter build web --release --dart-define=PULSEBOARD_BASE_URL=
66 modal deploy deploy/modal_app.py
77 ```
88
@@ -23,7 +23,7 @@ the deployment ships the previous client.
2323
2424 ## The token is not optional here
2525
26-Without `GLEAN_TOKEN` the reader binds loopback, Modal's proxy reaches
26+Without `PULSEBOARD_TOKEN` the reader binds loopback, Modal's proxy reaches
2727 nothing, and the deploy looks broken rather than insecure. The function
2828 raises with the reason instead of starting.
2929
@@ -1,8 +1,8 @@
1 # Deploying1 # Deploying
2 2
3 ```3 ```
4-modal secret create glean-reader GLEAN_TOKEN="$(openssl rand -hex 24)"4+modal secret create pulseboard PULSEBOARD_TOKEN="$(openssl rand -hex 24)"
5-cd app && flutter build web --release --dart-define=GLEAN_BASE_URL=5+cd app && flutter build web --release --dart-define=PULSEBOARD_BASE_URL=
6 modal deploy deploy/modal_app.py6 modal deploy deploy/modal_app.py
7 ```7 ```
8 8
@@ -23,7 +23,7 @@ the deployment ships the previous client.
23 23
24 ## The token is not optional here24 ## The token is not optional here
25 25
26-Without `GLEAN_TOKEN` the reader binds loopback, Modal's proxy reaches26+Without `PULSEBOARD_TOKEN` the reader binds loopback, Modal's proxy reaches
27 nothing, and the deploy looks broken rather than insecure. The function27 nothing, and the deploy looks broken rather than insecure. The function
28 raises with the reason instead of starting.28 raises with the reason instead of starting.
29 29
renamed nandi-pulseboard-subscriptions.opml +1 -1
similarity index 97%
rename from nandi-glean-subscriptions.opml
rename to nandi-pulseboard-subscriptions.opml
@@ -1,6 +1,6 @@
11 <?xml version="1.0" encoding="UTF-8"?>
22 <opml version="2.0">
3- <head><title>Glean subscriptions — nandi.uk</title></head>
3+ <head><title>Pulseboard subscriptions — nandi.uk</title></head>
44 <body>
55 <outline text="(iterate think thoughts)" title="(iterate think thoughts)" type="rss" xmlUrl="https://yogthos.net/feed.xml"/>
66 <outline text="This Week in GNOME" title="This Week in GNOME" type="rss" xmlUrl="https://thisweek.gnome.org/index.xml"/>
similarity index 97%
rename from nandi-glean-subscriptions.opml
rename to nandi-pulseboard-subscriptions.opml
@@ -1,6 +1,6 @@
1 <?xml version="1.0" encoding="UTF-8"?>1 <?xml version="1.0" encoding="UTF-8"?>
2 <opml version="2.0">2 <opml version="2.0">
3- <head><title>Glean subscriptions — nandi.uk</title></head>3+ <head><title>Pulseboard subscriptions — nandi.uk</title></head>
4 <body>4 <body>
5 <outline text="(iterate think thoughts)" title="(iterate think thoughts)" type="rss" xmlUrl="https://yogthos.net/feed.xml"/>5 <outline text="(iterate think thoughts)" title="(iterate think thoughts)" type="rss" xmlUrl="https://yogthos.net/feed.xml"/>
6 <outline text="This Week in GNOME" title="This Week in GNOME" type="rss" xmlUrl="https://thisweek.gnome.org/index.xml"/>6 <outline text="This Week in GNOME" title="This Week in GNOME" type="rss" xmlUrl="https://thisweek.gnome.org/index.xml"/>
modified readme.md +4 -4
@@ -1,4 +1,4 @@
1-# glean
1+# pulseboard
22
33 An RSS reader. Two pieces:
44
@@ -10,15 +10,15 @@ An RSS reader. Two pieces:
1010 Run the reader, point the client at it:
1111
1212 ```
13-cd spike/nim && nim c -d:ssl glean.nim && ./glean
13+cd spike/nim && nim c -d:ssl pulseboard.nim && ./pulseboard
1414 ```
1515
1616 It listens on `127.0.0.1:8080`, keeps a SQLite database under
17-`~/.local/share/glean/`, and serves a plain HTML interface at the root — so
17+`~/.local/share/pulseboard/`, and serves a plain HTML interface at the root — so
1818 it is usable on its own, without the Flutter client.
1919
2020 ```
21-cd app && flutter run --dart-define=GLEAN_BASE_URL=http://127.0.0.1:8080
21+cd app && flutter run --dart-define=PULSEBOARD_BASE_URL=http://127.0.0.1:8080
2222 ```
2323
2424 ## Why it is split
@@ -1,4 +1,4 @@
1-# glean1+# pulseboard
2 2
3 An RSS reader. Two pieces:3 An RSS reader. Two pieces:
4 4
@@ -10,15 +10,15 @@ An RSS reader. Two pieces:
10 Run the reader, point the client at it:10 Run the reader, point the client at it:
11 11
12 ```12 ```
13-cd spike/nim && nim c -d:ssl glean.nim && ./glean13+cd spike/nim && nim c -d:ssl pulseboard.nim && ./pulseboard
14 ```14 ```
15 15
16 It listens on `127.0.0.1:8080`, keeps a SQLite database under16 It listens on `127.0.0.1:8080`, keeps a SQLite database under
17-`~/.local/share/glean/`, and serves a plain HTML interface at the root — so17+`~/.local/share/pulseboard/`, and serves a plain HTML interface at the root — so
18 it is usable on its own, without the Flutter client.18 it is usable on its own, without the Flutter client.
19 19
20 ```20 ```
21-cd app && flutter run --dart-define=GLEAN_BASE_URL=http://127.0.0.1:808021+cd app && flutter run --dart-define=PULSEBOARD_BASE_URL=http://127.0.0.1:8080
22 ```22 ```
23 23
24 ## Why it is split24 ## Why it is split
modified spike/nim/feedparser_probe.nim +1 -1
@@ -229,7 +229,7 @@ proc main() =
229229 for url in feeds:
230230 try:
231231 let client = newHttpClient(timeout = 20_000,
232- userAgent = "glean-nim-spike/0.1")
232+ userAgent = "pulseboard-nim-spike/0.1")
233233 defer: client.close()
234234 let body = client.getContent(url)
235235 let r = parseFeed(body, url)
@@ -229,7 +229,7 @@ proc main() =
229 for url in feeds:229 for url in feeds:
230 try:230 try:
231 let client = newHttpClient(timeout = 20_000,231 let client = newHttpClient(timeout = 20_000,
232- userAgent = "glean-nim-spike/0.1")232+ userAgent = "pulseboard-nim-spike/0.1")
233 defer: client.close()233 defer: client.close()
234 let body = client.getContent(url)234 let body = client.getContent(url)
235 let r = parseFeed(body, url)235 let r = parseFeed(body, url)
renamed spike/nim/pulseboard.nim +19 -19
similarity index 93%
rename from spike/nim/glean.nim
rename to spike/nim/pulseboard.nim
@@ -1,19 +1,19 @@
11 ## A small RSS reader.
22 ##
3-## nim c -r -d:ssl glean.nim
4-## GLEAN_DB=~/.local/share/glean/db GLEAN_PORT=8080 ./glean
3+## nim c -r -d:ssl pulseboard.nim
4+## PULSEBOARD_DB=~/.local/share/pulseboard/db PULSEBOARD_PORT=8080 ./pulseboard
55 ##
66 ## Single user, no accounts. It runs on your machine and reads your feeds,
77 ## and that assumption is what keeps this a few hundred lines instead of a
88 ## few thousand: no sessions, no CSRF, no per-user scoping, no ATProto.
99 ##
10-## GLEAN_TOKEN puts a shared secret in front of everything, which is what
10+## PULSEBOARD_TOKEN puts a shared secret in front of everything, which is what
1111 ## makes it safe anywhere but loopback -- `/feeds` and `/fetch-content` will
1212 ## fetch any URL handed to them, so an open instance is an SSRF proxy as well
1313 ## as someone else's feed list. Unset, there is no gate at all and the local
1414 ## case stays exactly as simple as it was.
1515 ##
16-## GLEAN_WEB must point at the Flutter web build. The reader refuses to start
16+## PULSEBOARD_WEB must point at the Flutter web build. The reader refuses to start
1717 ## without it so deployments cannot silently serve a different client.
1818 ##
1919 ## The pieces underneath -- fetching, parsing, storage, search, full-text
@@ -21,7 +21,7 @@
2121
2222 import std/[asyncdispatch, asynchttpserver, httpclient, json, options, os,
2323 strformat, strutils, times, uri]
24-import reader/[articlestore, feedfetcher, feedparser, feedstore, gleandb,
24+import reader/[articlestore, feedfetcher, feedparser, feedstore, pulseboarddb,
2525 opml, scraper, sqlite]
2626
2727 const
@@ -32,7 +32,7 @@ const
3232 DefaultPort = 8080
3333
3434 type Reader = ref object
35- db: GleanDb
35+ db: PulseboardDb
3636 token: string ## empty disables the gate entirely
3737 webRoot: string ## required Flutter web bundle
3838
@@ -57,10 +57,10 @@ proc presentedToken(req: Request, queryKey: string): string =
5757 ##
5858 ## Three ways because three callers: a native client sends a header, a
5959 ## person opens a link, and the browser then has a cookie.
60- let h = req.headers.getOrDefault("X-Glean-Token").string
60+ let h = req.headers.getOrDefault("X-Pulseboard-Token").string
6161 if h.len > 0: return h
6262 if queryKey.len > 0: return queryKey
63- cookieValue(req.headers.getOrDefault("Cookie").string, "glean_token")
63+ cookieValue(req.headers.getOrDefault("Cookie").string, "pulseboard_token")
6464
6565 proc jsonResponse(req: Request, code: HttpCode, node: JsonNode) {.async.} =
6666 await req.respond(code, $node, newHttpHeaders({
@@ -208,7 +208,7 @@ proc handle(r: Reader, req: Request) {.async.} =
208208 if param("key").len > 0:
209209 await req.respond(Http303, "", newHttpHeaders({
210210 "Location": "/",
211- "Set-Cookie": "glean_token=" & r.token &
211+ "Set-Cookie": "pulseboard_token=" & r.token &
212212 "; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000",
213213 }))
214214 return
@@ -233,7 +233,7 @@ proc handle(r: Reader, req: Request) {.async.} =
233233 feeds.add OpmlFeed(url: s.feedUrl, title: s.feedTitle, category: s.category)
234234 await req.respond(Http200, renderOpml(feeds), newHttpHeaders({
235235 "Content-Type": "application/xml; charset=utf-8",
236- "Content-Disposition": "attachment; filename=glean-subscriptions.opml",
236+ "Content-Disposition": "attachment; filename=pulseboard-subscriptions.opml",
237237 }))
238238
239239 of "articles":
@@ -351,7 +351,7 @@ proc handle(r: Reader, req: Request) {.async.} =
351351 return
352352 try:
353353 let client = newHttpClient(timeout = 20_000,
354- userAgent = "glean/0.1")
354+ userAgent = "pulseboard/0.1")
355355 defer: client.close()
356356 let text = extractArticle(client.getContent(a.get.url))
357357 r.db.updateArticleFullContent(id, text)
@@ -372,25 +372,25 @@ proc handle(r: Reader, req: Request) {.async.} =
372372 await fail(req, Http405, "method not allowed")
373373
374374 proc main() {.async.} =
375- let dbPath = getEnv("GLEAN_DB", getHomeDir() / ".local/share/glean/glean")
375+ let dbPath = getEnv("PULSEBOARD_DB", getHomeDir() / ".local/share/pulseboard/pulseboard")
376376 createDir(dbPath.parentDir)
377- let port = try: parseInt(getEnv("GLEAN_PORT", $DefaultPort))
377+ let port = try: parseInt(getEnv("PULSEBOARD_PORT", $DefaultPort))
378378 except ValueError: DefaultPort
379379
380- var db = gleandb.open(dbPath)
380+ var db = pulseboarddb.open(dbPath)
381381 db.migrate()
382382 db.db.run("INSERT OR IGNORE INTO users (did) VALUES (?)", p(LocalUser))
383383
384- let token = getEnv("GLEAN_TOKEN")
385- let webRoot = getEnv("GLEAN_WEB")
384+ let token = getEnv("PULSEBOARD_TOKEN")
385+ let webRoot = getEnv("PULSEBOARD_WEB")
386386 if webRoot.len == 0:
387- quit("GLEAN_WEB is required and must point at a Flutter web build")
387+ quit("PULSEBOARD_WEB is required and must point at a Flutter web build")
388388 if not dirExists(webRoot) or not fileExists(webRoot / "index.html"):
389- quit(&"GLEAN_WEB points at {webRoot}, which has no index.html")
389+ quit(&"PULSEBOARD_WEB points at {webRoot}, which has no index.html")
390390
391391 let reader = Reader(db: db, token: token, webRoot: webRoot)
392392
393- echo &"glean reading from {dbPath}"
393+ echo &"pulseboard reading from {dbPath}"
394394 if webRoot.len > 0: echo &"serving the client from {webRoot}"
395395 if token.len > 0:
396396 echo "token required: append ?key=… once to set the cookie"
similarity index 93%
rename from spike/nim/glean.nim
rename to spike/nim/pulseboard.nim
@@ -1,19 +1,19 @@
1 ## A small RSS reader.1 ## A small RSS reader.
2 ##2 ##
3-## nim c -r -d:ssl glean.nim3+## nim c -r -d:ssl pulseboard.nim
4-## GLEAN_DB=~/.local/share/glean/db GLEAN_PORT=8080 ./glean4+## PULSEBOARD_DB=~/.local/share/pulseboard/db PULSEBOARD_PORT=8080 ./pulseboard
5 ##5 ##
6 ## Single user, no accounts. It runs on your machine and reads your feeds,6 ## Single user, no accounts. It runs on your machine and reads your feeds,
7 ## and that assumption is what keeps this a few hundred lines instead of a7 ## and that assumption is what keeps this a few hundred lines instead of a
8 ## few thousand: no sessions, no CSRF, no per-user scoping, no ATProto.8 ## few thousand: no sessions, no CSRF, no per-user scoping, no ATProto.
9 ##9 ##
10-## GLEAN_TOKEN puts a shared secret in front of everything, which is what10+## PULSEBOARD_TOKEN puts a shared secret in front of everything, which is what
11 ## makes it safe anywhere but loopback -- `/feeds` and `/fetch-content` will11 ## makes it safe anywhere but loopback -- `/feeds` and `/fetch-content` will
12 ## fetch any URL handed to them, so an open instance is an SSRF proxy as well12 ## fetch any URL handed to them, so an open instance is an SSRF proxy as well
13 ## as someone else's feed list. Unset, there is no gate at all and the local13 ## as someone else's feed list. Unset, there is no gate at all and the local
14 ## case stays exactly as simple as it was.14 ## case stays exactly as simple as it was.
15 ##15 ##
16-## GLEAN_WEB must point at the Flutter web build. The reader refuses to start16+## PULSEBOARD_WEB must point at the Flutter web build. The reader refuses to start
17 ## without it so deployments cannot silently serve a different client.17 ## without it so deployments cannot silently serve a different client.
18 ##18 ##
19 ## The pieces underneath -- fetching, parsing, storage, search, full-text19 ## The pieces underneath -- fetching, parsing, storage, search, full-text
@@ -21,7 +21,7 @@
21 21
22 import std/[asyncdispatch, asynchttpserver, httpclient, json, options, os,22 import std/[asyncdispatch, asynchttpserver, httpclient, json, options, os,
23 strformat, strutils, times, uri]23 strformat, strutils, times, uri]
24-import reader/[articlestore, feedfetcher, feedparser, feedstore, gleandb,24+import reader/[articlestore, feedfetcher, feedparser, feedstore, pulseboarddb,
25 opml, scraper, sqlite]25 opml, scraper, sqlite]
26 26
27 const27 const
@@ -32,7 +32,7 @@ const
32 DefaultPort = 808032 DefaultPort = 8080
33 33
34 type Reader = ref object34 type Reader = ref object
35- db: GleanDb35+ db: PulseboardDb
36 token: string ## empty disables the gate entirely36 token: string ## empty disables the gate entirely
37 webRoot: string ## required Flutter web bundle37 webRoot: string ## required Flutter web bundle
38 38
@@ -57,10 +57,10 @@ proc presentedToken(req: Request, queryKey: string): string =
57 ##57 ##
58 ## Three ways because three callers: a native client sends a header, a58 ## Three ways because three callers: a native client sends a header, a
59 ## person opens a link, and the browser then has a cookie.59 ## person opens a link, and the browser then has a cookie.
60- let h = req.headers.getOrDefault("X-Glean-Token").string60+ let h = req.headers.getOrDefault("X-Pulseboard-Token").string
61 if h.len > 0: return h61 if h.len > 0: return h
62 if queryKey.len > 0: return queryKey62 if queryKey.len > 0: return queryKey
63- cookieValue(req.headers.getOrDefault("Cookie").string, "glean_token")63+ cookieValue(req.headers.getOrDefault("Cookie").string, "pulseboard_token")
64 64
65 proc jsonResponse(req: Request, code: HttpCode, node: JsonNode) {.async.} =65 proc jsonResponse(req: Request, code: HttpCode, node: JsonNode) {.async.} =
66 await req.respond(code, $node, newHttpHeaders({66 await req.respond(code, $node, newHttpHeaders({
@@ -208,7 +208,7 @@ proc handle(r: Reader, req: Request) {.async.} =
208 if param("key").len > 0:208 if param("key").len > 0:
209 await req.respond(Http303, "", newHttpHeaders({209 await req.respond(Http303, "", newHttpHeaders({
210 "Location": "/",210 "Location": "/",
211- "Set-Cookie": "glean_token=" & r.token &211+ "Set-Cookie": "pulseboard_token=" & r.token &
212 "; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000",212 "; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000",
213 }))213 }))
214 return214 return
@@ -233,7 +233,7 @@ proc handle(r: Reader, req: Request) {.async.} =
233 feeds.add OpmlFeed(url: s.feedUrl, title: s.feedTitle, category: s.category)233 feeds.add OpmlFeed(url: s.feedUrl, title: s.feedTitle, category: s.category)
234 await req.respond(Http200, renderOpml(feeds), newHttpHeaders({234 await req.respond(Http200, renderOpml(feeds), newHttpHeaders({
235 "Content-Type": "application/xml; charset=utf-8",235 "Content-Type": "application/xml; charset=utf-8",
236- "Content-Disposition": "attachment; filename=glean-subscriptions.opml",236+ "Content-Disposition": "attachment; filename=pulseboard-subscriptions.opml",
237 }))237 }))
238 238
239 of "articles":239 of "articles":
@@ -351,7 +351,7 @@ proc handle(r: Reader, req: Request) {.async.} =
351 return351 return
352 try:352 try:
353 let client = newHttpClient(timeout = 20_000,353 let client = newHttpClient(timeout = 20_000,
354- userAgent = "glean/0.1")354+ userAgent = "pulseboard/0.1")
355 defer: client.close()355 defer: client.close()
356 let text = extractArticle(client.getContent(a.get.url))356 let text = extractArticle(client.getContent(a.get.url))
357 r.db.updateArticleFullContent(id, text)357 r.db.updateArticleFullContent(id, text)
@@ -372,25 +372,25 @@ proc handle(r: Reader, req: Request) {.async.} =
372 await fail(req, Http405, "method not allowed")372 await fail(req, Http405, "method not allowed")
373 373
374 proc main() {.async.} =374 proc main() {.async.} =
375- let dbPath = getEnv("GLEAN_DB", getHomeDir() / ".local/share/glean/glean")375+ let dbPath = getEnv("PULSEBOARD_DB", getHomeDir() / ".local/share/pulseboard/pulseboard")
376 createDir(dbPath.parentDir)376 createDir(dbPath.parentDir)
377- let port = try: parseInt(getEnv("GLEAN_PORT", $DefaultPort))377+ let port = try: parseInt(getEnv("PULSEBOARD_PORT", $DefaultPort))
378 except ValueError: DefaultPort378 except ValueError: DefaultPort
379 379
380- var db = gleandb.open(dbPath)380+ var db = pulseboarddb.open(dbPath)
381 db.migrate()381 db.migrate()
382 db.db.run("INSERT OR IGNORE INTO users (did) VALUES (?)", p(LocalUser))382 db.db.run("INSERT OR IGNORE INTO users (did) VALUES (?)", p(LocalUser))
383 383
384- let token = getEnv("GLEAN_TOKEN")384+ let token = getEnv("PULSEBOARD_TOKEN")
385- let webRoot = getEnv("GLEAN_WEB")385+ let webRoot = getEnv("PULSEBOARD_WEB")
386 if webRoot.len == 0:386 if webRoot.len == 0:
387- quit("GLEAN_WEB is required and must point at a Flutter web build")387+ quit("PULSEBOARD_WEB is required and must point at a Flutter web build")
388 if not dirExists(webRoot) or not fileExists(webRoot / "index.html"):388 if not dirExists(webRoot) or not fileExists(webRoot / "index.html"):
389- quit(&"GLEAN_WEB points at {webRoot}, which has no index.html")389+ quit(&"PULSEBOARD_WEB points at {webRoot}, which has no index.html")
390 390
391 let reader = Reader(db: db, token: token, webRoot: webRoot)391 let reader = Reader(db: db, token: token, webRoot: webRoot)
392 392
393- echo &"glean reading from {dbPath}"393+ echo &"pulseboard reading from {dbPath}"
394 if webRoot.len > 0: echo &"serving the client from {webRoot}"394 if webRoot.len > 0: echo &"serving the client from {webRoot}"
395 if token.len > 0:395 if token.len > 0:
396 echo "token required: append ?key=… once to set the cookie"396 echo "token required: append ?key=… once to set the cookie"
renamed spike/nim/pulseboard.nimble +3 -3
similarity index 93%
rename from spike/nim/glean.nimble
rename to spike/nim/pulseboard.nimble
@@ -1,6 +1,6 @@
11 version = "0.1.0"
2-author = "glean"
3-description = "glean: a small RSS reader"
2+author = "pulseboard"
3+description = "pulseboard: a small RSS reader"
44 license = "MIT"
55 srcDir = "."
66
@@ -8,7 +8,7 @@ requires "nim >= 2.2.0"
88
99 # Probes that touch only this machine. Safe to run anywhere, including CI.
1010 const offlineProbes = @[
11- "gleandb_probe",
11+ "pulseboarddb_probe",
1212 "stores_probe",
1313 ]
1414
similarity index 93%
rename from spike/nim/glean.nimble
rename to spike/nim/pulseboard.nimble
@@ -1,6 +1,6 @@
1 version = "0.1.0"1 version = "0.1.0"
2-author = "glean"2+author = "pulseboard"
3-description = "glean: a small RSS reader"3+description = "pulseboard: a small RSS reader"
4 license = "MIT"4 license = "MIT"
5 srcDir = "."5 srcDir = "."
6 6
@@ -8,7 +8,7 @@ requires "nim >= 2.2.0"
8 8
9 # Probes that touch only this machine. Safe to run anywhere, including CI.9 # Probes that touch only this machine. Safe to run anywhere, including CI.
10 const offlineProbes = @[10 const offlineProbes = @[
11- "gleandb_probe",11+ "pulseboarddb_probe",
12 "stores_probe",12 "stores_probe",
13 ]13 ]
14 14
renamed spike/nim/pulseboarddb_probe.nim +6 -6
similarity index 94%
rename from spike/nim/gleandb_probe.nim
rename to spike/nim/pulseboarddb_probe.nim
@@ -1,7 +1,7 @@
11 ## Checks the three-database layout, the schema, and the behaviour that
22 ## depends on both.
33 ##
4-## nim c -r -d:ssl gleandb_probe.nim
4+## nim c -r -d:ssl pulseboarddb_probe.nim
55 ##
66 ## Most of this is about things that fail quietly. An ATTACH that did not
77 ## happen, a pragma that did not take, an FTS index whose triggers never fire
@@ -9,7 +9,7 @@
99 ## degrades or diverges later.
1010
1111 import std/[options, os, strformat, strutils]
12-import reader/[gleandb, schema, sqlite]
12+import reader/[pulseboarddb, schema, sqlite]
1313
1414 var failures = 0
1515
@@ -25,14 +25,14 @@ proc cleanup(base: string) =
2525 removeFile(base & suffix & ext)
2626
2727 proc main() =
28- echo "Glean database layer probe"
28+ echo "Pulseboard database layer probe"
2929 echo ""
3030
31- let base = getTempDir() / "glean_db_probe"
31+ let base = getTempDir() / "pulseboard_db_probe"
3232 cleanup(base)
3333 defer: cleanup(base)
3434
35- var g = gleandb.open(base)
35+ var g = pulseboarddb.open(base)
3636 defer: g.close()
3737
3838 report("the schema covers the reader's tables", readerSchema.len == 15,
@@ -126,7 +126,7 @@ proc main() =
126126
127127 echo ""
128128 if failures == 0:
129- echo "RESULT: the database layer matches Glean's layout and behaviour."
129+ echo "RESULT: the database layer matches Pulseboard's layout and behaviour."
130130 else:
131131 echo &"RESULT: {failures} check(s) failed."
132132 quit 1
similarity index 94%
rename from spike/nim/gleandb_probe.nim
rename to spike/nim/pulseboarddb_probe.nim
@@ -1,7 +1,7 @@
1 ## Checks the three-database layout, the schema, and the behaviour that1 ## Checks the three-database layout, the schema, and the behaviour that
2 ## depends on both.2 ## depends on both.
3 ##3 ##
4-## nim c -r -d:ssl gleandb_probe.nim4+## nim c -r -d:ssl pulseboarddb_probe.nim
5 ##5 ##
6 ## Most of this is about things that fail quietly. An ATTACH that did not6 ## Most of this is about things that fail quietly. An ATTACH that did not
7 ## happen, a pragma that did not take, an FTS index whose triggers never fire7 ## happen, a pragma that did not take, an FTS index whose triggers never fire
@@ -9,7 +9,7 @@
9 ## degrades or diverges later.9 ## degrades or diverges later.
10 10
11 import std/[options, os, strformat, strutils]11 import std/[options, os, strformat, strutils]
12-import reader/[gleandb, schema, sqlite]12+import reader/[pulseboarddb, schema, sqlite]
13 13
14 var failures = 014 var failures = 0
15 15
@@ -25,14 +25,14 @@ proc cleanup(base: string) =
25 removeFile(base & suffix & ext)25 removeFile(base & suffix & ext)
26 26
27 proc main() =27 proc main() =
28- echo "Glean database layer probe"28+ echo "Pulseboard database layer probe"
29 echo ""29 echo ""
30 30
31- let base = getTempDir() / "glean_db_probe"31+ let base = getTempDir() / "pulseboard_db_probe"
32 cleanup(base)32 cleanup(base)
33 defer: cleanup(base)33 defer: cleanup(base)
34 34
35- var g = gleandb.open(base)35+ var g = pulseboarddb.open(base)
36 defer: g.close()36 defer: g.close()
37 37
38 report("the schema covers the reader's tables", readerSchema.len == 15,38 report("the schema covers the reader's tables", readerSchema.len == 15,
@@ -126,7 +126,7 @@ proc main() =
126 126
127 echo ""127 echo ""
128 if failures == 0:128 if failures == 0:
129- echo "RESULT: the database layer matches Glean's layout and behaviour."129+ echo "RESULT: the database layer matches Pulseboard's layout and behaviour."
130 else:130 else:
131 echo &"RESULT: {failures} check(s) failed."131 echo &"RESULT: {failures} check(s) failed."
132 quit 1132 quit 1
modified spike/nim/reader/articlestore.nim +14 -14
@@ -7,7 +7,7 @@
77
88 import std/[options, strutils, times]
99 import ./feedstore
10-import ./gleandb
10+import ./pulseboarddb
1111 import ./sqlite
1212
1313 type
@@ -78,15 +78,15 @@ const articleFrom = """
7878 LEFT JOIN articles.feeds f ON f.feed_url = a.feed_url
7979 LEFT JOIN articles.read_state r ON r.article_id = a.id AND r.user_did = ?"""
8080
81-proc getArticle*(g: GleanDb, userDid: string, id: int64): Option[Article] =
81+proc getArticle*(g: PulseboardDb, userDid: string, id: int64): Option[Article] =
8282 g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.id = ?",
8383 [p(userDid), p(id)], readArticleRow)
8484
85-proc getArticleByUrl*(g: GleanDb, userDid, url: string): Option[Article] =
85+proc getArticleByUrl*(g: PulseboardDb, userDid, url: string): Option[Article] =
8686 g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.url = ?",
8787 [p(userDid), p(url)], readArticleRow)
8888
89-proc listArticles*(g: GleanDb, userDid: string, filter = afAll,
89+proc listArticles*(g: PulseboardDb, userDid: string, filter = afAll,
9090 feedUrl = "", category = "", sortOldest = false,
9191 limit = 25, offset = 0): seq[Article] =
9292 ## The main listing, scoped to what the user subscribes to.
@@ -120,7 +120,7 @@ proc listArticles*(g: GleanDb, userDid: string, filter = afAll,
120120
121121 g.db.queryAll(sql, params, readArticleRow)
122122
123-proc searchArticles*(g: GleanDb, userDid, query: string,
123+proc searchArticles*(g: PulseboardDb, userDid, query: string,
124124 limit = 25, offset = 0): seq[Article] =
125125 ## Full-text search over the user's subscriptions.
126126 ##
@@ -143,7 +143,7 @@ proc searchArticles*(g: GleanDb, userDid, query: string,
143143 [p(userDid), p(userDid), p(query),
144144 p(limit.int64), p(offset.int64)], readArticleRow)
145145
146-proc batchUpsertArticles*(g: GleanDb, articles: seq[NewArticle],
146+proc batchUpsertArticles*(g: PulseboardDb, articles: seq[NewArticle],
147147 retentionDays = 0) =
148148 ## Ingest a fetch. One transaction, one prepared statement.
149149 ##
@@ -191,13 +191,13 @@ proc batchUpsertArticles*(g: GleanDb, articles: seq[NewArticle],
191191 for url in feedUrls:
192192 restore.exec(p(url))
193193
194-proc updateArticleFullContent*(g: GleanDb, id: int64, fullContent: string) =
194+proc updateArticleFullContent*(g: PulseboardDb, id: int64, fullContent: string) =
195195 g.db.run("UPDATE articles.articles SET full_content = ? WHERE id = ?",
196196 pOrNull(fullContent), p(id))
197197
198198 # --- read state ------------------------------------------------------------
199199
200-proc setRead(g: GleanDb, userDid: string, articleId: int64, isRead: bool) =
200+proc setRead(g: PulseboardDb, userDid: string, articleId: int64, isRead: bool) =
201201 g.db.run("""INSERT INTO articles.read_state (user_did, article_id, is_read, read_at)
202202 VALUES (?, ?, ?, CURRENT_TIMESTAMP)
203203 ON CONFLICT(user_did, article_id) DO UPDATE SET
@@ -205,13 +205,13 @@ proc setRead(g: GleanDb, userDid: string, articleId: int64, isRead: bool) =
205205 read_at = excluded.read_at""",
206206 p(userDid), p(articleId), p(if isRead: 1'i64 else: 0'i64))
207207
208-proc markArticleRead*(g: GleanDb, userDid: string, articleId: int64) =
208+proc markArticleRead*(g: PulseboardDb, userDid: string, articleId: int64) =
209209 setRead(g, userDid, articleId, true)
210210
211-proc markArticleUnread*(g: GleanDb, userDid: string, articleId: int64) =
211+proc markArticleUnread*(g: PulseboardDb, userDid: string, articleId: int64) =
212212 setRead(g, userDid, articleId, false)
213213
214-proc markAllRead*(g: GleanDb, userDid, feedUrl: string) =
214+proc markAllRead*(g: PulseboardDb, userDid, feedUrl: string) =
215215 ## Scoped to one feed when given, otherwise everything the user subscribes
216216 ## to. Getting this scope wrong is the difference between clearing a feed
217217 ## and clearing the whole account, so the two cases are separate statements
@@ -234,7 +234,7 @@ proc markAllRead*(g: GleanDb, userDid, feedUrl: string) =
234234 is_read = 1, read_at = CURRENT_TIMESTAMP""",
235235 p(userDid), p(userDid))
236236
237-proc getUnreadCount*(g: GleanDb, userDid, feedUrl, category: string): int =
237+proc getUnreadCount*(g: PulseboardDb, userDid, feedUrl, category: string): int =
238238 var sql = """
239239 SELECT count(*)
240240 FROM articles.articles a
@@ -252,12 +252,12 @@ proc getUnreadCount*(g: GleanDb, userDid, feedUrl, category: string): int =
252252 params.add p(category)
253253 g.db.queryInt(sql, params).get(0).int
254254
255-proc isRead*(g: GleanDb, userDid: string, articleId: int64): bool =
255+proc isRead*(g: PulseboardDb, userDid: string, articleId: int64): bool =
256256 g.db.queryInt("""SELECT COALESCE(is_read, 0) FROM articles.read_state
257257 WHERE user_did = ? AND article_id = ?""",
258258 p(userDid), p(articleId)).get(0) != 0
259259
260-proc countNewArticles*(g: GleanDb, userDid: string, since: DateTime): int =
260+proc countNewArticles*(g: PulseboardDb, userDid: string, since: DateTime): int =
261261 g.db.queryInt("""
262262 SELECT count(*) FROM articles.articles a
263263 JOIN articles.subscriptions s
@@ -7,7 +7,7 @@
7 7
8 import std/[options, strutils, times]8 import std/[options, strutils, times]
9 import ./feedstore9 import ./feedstore
10-import ./gleandb10+import ./pulseboarddb
11 import ./sqlite11 import ./sqlite
12 12
13 type13 type
@@ -78,15 +78,15 @@ const articleFrom = """
78 LEFT JOIN articles.feeds f ON f.feed_url = a.feed_url78 LEFT JOIN articles.feeds f ON f.feed_url = a.feed_url
79 LEFT JOIN articles.read_state r ON r.article_id = a.id AND r.user_did = ?"""79 LEFT JOIN articles.read_state r ON r.article_id = a.id AND r.user_did = ?"""
80 80
81-proc getArticle*(g: GleanDb, userDid: string, id: int64): Option[Article] =81+proc getArticle*(g: PulseboardDb, userDid: string, id: int64): Option[Article] =
82 g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.id = ?",82 g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.id = ?",
83 [p(userDid), p(id)], readArticleRow)83 [p(userDid), p(id)], readArticleRow)
84 84
85-proc getArticleByUrl*(g: GleanDb, userDid, url: string): Option[Article] =85+proc getArticleByUrl*(g: PulseboardDb, userDid, url: string): Option[Article] =
86 g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.url = ?",86 g.db.queryFirst("SELECT " & articleColumns & articleFrom & " WHERE a.url = ?",
87 [p(userDid), p(url)], readArticleRow)87 [p(userDid), p(url)], readArticleRow)
88 88
89-proc listArticles*(g: GleanDb, userDid: string, filter = afAll,89+proc listArticles*(g: PulseboardDb, userDid: string, filter = afAll,
90 feedUrl = "", category = "", sortOldest = false,90 feedUrl = "", category = "", sortOldest = false,
91 limit = 25, offset = 0): seq[Article] =91 limit = 25, offset = 0): seq[Article] =
92 ## The main listing, scoped to what the user subscribes to.92 ## The main listing, scoped to what the user subscribes to.
@@ -120,7 +120,7 @@ proc listArticles*(g: GleanDb, userDid: string, filter = afAll,
120 120
121 g.db.queryAll(sql, params, readArticleRow)121 g.db.queryAll(sql, params, readArticleRow)
122 122
123-proc searchArticles*(g: GleanDb, userDid, query: string,123+proc searchArticles*(g: PulseboardDb, userDid, query: string,
124 limit = 25, offset = 0): seq[Article] =124 limit = 25, offset = 0): seq[Article] =
125 ## Full-text search over the user's subscriptions.125 ## Full-text search over the user's subscriptions.
126 ##126 ##
@@ -143,7 +143,7 @@ proc searchArticles*(g: GleanDb, userDid, query: string,
143 [p(userDid), p(userDid), p(query),143 [p(userDid), p(userDid), p(query),
144 p(limit.int64), p(offset.int64)], readArticleRow)144 p(limit.int64), p(offset.int64)], readArticleRow)
145 145
146-proc batchUpsertArticles*(g: GleanDb, articles: seq[NewArticle],146+proc batchUpsertArticles*(g: PulseboardDb, articles: seq[NewArticle],
147 retentionDays = 0) =147 retentionDays = 0) =
148 ## Ingest a fetch. One transaction, one prepared statement.148 ## Ingest a fetch. One transaction, one prepared statement.
149 ##149 ##
@@ -191,13 +191,13 @@ proc batchUpsertArticles*(g: GleanDb, articles: seq[NewArticle],
191 for url in feedUrls:191 for url in feedUrls:
192 restore.exec(p(url))192 restore.exec(p(url))
193 193
194-proc updateArticleFullContent*(g: GleanDb, id: int64, fullContent: string) =194+proc updateArticleFullContent*(g: PulseboardDb, id: int64, fullContent: string) =
195 g.db.run("UPDATE articles.articles SET full_content = ? WHERE id = ?",195 g.db.run("UPDATE articles.articles SET full_content = ? WHERE id = ?",
196 pOrNull(fullContent), p(id))196 pOrNull(fullContent), p(id))
197 197
198 # --- read state ------------------------------------------------------------198 # --- read state ------------------------------------------------------------
199 199
200-proc setRead(g: GleanDb, userDid: string, articleId: int64, isRead: bool) =200+proc setRead(g: PulseboardDb, userDid: string, articleId: int64, isRead: bool) =
201 g.db.run("""INSERT INTO articles.read_state (user_did, article_id, is_read, read_at)201 g.db.run("""INSERT INTO articles.read_state (user_did, article_id, is_read, read_at)
202 VALUES (?, ?, ?, CURRENT_TIMESTAMP)202 VALUES (?, ?, ?, CURRENT_TIMESTAMP)
203 ON CONFLICT(user_did, article_id) DO UPDATE SET203 ON CONFLICT(user_did, article_id) DO UPDATE SET
@@ -205,13 +205,13 @@ proc setRead(g: GleanDb, userDid: string, articleId: int64, isRead: bool) =
205 read_at = excluded.read_at""",205 read_at = excluded.read_at""",
206 p(userDid), p(articleId), p(if isRead: 1'i64 else: 0'i64))206 p(userDid), p(articleId), p(if isRead: 1'i64 else: 0'i64))
207 207
208-proc markArticleRead*(g: GleanDb, userDid: string, articleId: int64) =208+proc markArticleRead*(g: PulseboardDb, userDid: string, articleId: int64) =
209 setRead(g, userDid, articleId, true)209 setRead(g, userDid, articleId, true)
210 210
211-proc markArticleUnread*(g: GleanDb, userDid: string, articleId: int64) =211+proc markArticleUnread*(g: PulseboardDb, userDid: string, articleId: int64) =
212 setRead(g, userDid, articleId, false)212 setRead(g, userDid, articleId, false)
213 213
214-proc markAllRead*(g: GleanDb, userDid, feedUrl: string) =214+proc markAllRead*(g: PulseboardDb, userDid, feedUrl: string) =
215 ## Scoped to one feed when given, otherwise everything the user subscribes215 ## Scoped to one feed when given, otherwise everything the user subscribes
216 ## to. Getting this scope wrong is the difference between clearing a feed216 ## to. Getting this scope wrong is the difference between clearing a feed
217 ## and clearing the whole account, so the two cases are separate statements217 ## and clearing the whole account, so the two cases are separate statements
@@ -234,7 +234,7 @@ proc markAllRead*(g: GleanDb, userDid, feedUrl: string) =
234 is_read = 1, read_at = CURRENT_TIMESTAMP""",234 is_read = 1, read_at = CURRENT_TIMESTAMP""",
235 p(userDid), p(userDid))235 p(userDid), p(userDid))
236 236
237-proc getUnreadCount*(g: GleanDb, userDid, feedUrl, category: string): int =237+proc getUnreadCount*(g: PulseboardDb, userDid, feedUrl, category: string): int =
238 var sql = """238 var sql = """
239 SELECT count(*)239 SELECT count(*)
240 FROM articles.articles a240 FROM articles.articles a
@@ -252,12 +252,12 @@ proc getUnreadCount*(g: GleanDb, userDid, feedUrl, category: string): int =
252 params.add p(category)252 params.add p(category)
253 g.db.queryInt(sql, params).get(0).int253 g.db.queryInt(sql, params).get(0).int
254 254
255-proc isRead*(g: GleanDb, userDid: string, articleId: int64): bool =255+proc isRead*(g: PulseboardDb, userDid: string, articleId: int64): bool =
256 g.db.queryInt("""SELECT COALESCE(is_read, 0) FROM articles.read_state256 g.db.queryInt("""SELECT COALESCE(is_read, 0) FROM articles.read_state
257 WHERE user_did = ? AND article_id = ?""",257 WHERE user_did = ? AND article_id = ?""",
258 p(userDid), p(articleId)).get(0) != 0258 p(userDid), p(articleId)).get(0) != 0
259 259
260-proc countNewArticles*(g: GleanDb, userDid: string, since: DateTime): int =260+proc countNewArticles*(g: PulseboardDb, userDid: string, since: DateTime): int =
261 g.db.queryInt("""261 g.db.queryInt("""
262 SELECT count(*) FROM articles.articles a262 SELECT count(*) FROM articles.articles a
263 JOIN articles.subscriptions s263 JOIN articles.subscriptions s
modified spike/nim/reader/feedfetcher.nim +1 -1
@@ -48,7 +48,7 @@ type
4848 const
4949 AcceptFeed* = "application/xml,application/atom+xml,application/rss+xml," &
5050 "application/rdf+xml,application/feed+json,text/html;q=0.9"
51- DefaultUserAgent* = "glean-nim/0.1"
51+ DefaultUserAgent* = "pulseboard-nim/0.1"
5252
5353 proc defaultConfig*(): FetchConfig =
5454 FetchConfig(
@@ -48,7 +48,7 @@ type
48 const48 const
49 AcceptFeed* = "application/xml,application/atom+xml,application/rss+xml," &49 AcceptFeed* = "application/xml,application/atom+xml,application/rss+xml," &
50 "application/rdf+xml,application/feed+json,text/html;q=0.9"50 "application/rdf+xml,application/feed+json,text/html;q=0.9"
51- DefaultUserAgent* = "glean-nim/0.1"51+ DefaultUserAgent* = "pulseboard-nim/0.1"
52 52
53 proc defaultConfig*(): FetchConfig =53 proc defaultConfig*(): FetchConfig =
54 FetchConfig(54 FetchConfig(
modified spike/nim/reader/feedstore.nim +17 -17
@@ -6,7 +6,7 @@
66 ## read on every listing and written only when someone subscribes.
77
88 import std/[options, times]
9-import ./gleandb
9+import ./pulseboarddb
1010 import ./sqlite
1111
1212 type
@@ -73,7 +73,7 @@ const feedColumns = """f.feed_url, f.title, f.site_url, f.description,
7373 f.feed_type, f.favicon_url, f.subscriber_count, f.error_count, f.last_error,
7474 f.last_fetched_at"""
7575
76-proc upsertFeed*(g: GleanDb, f: Feed) =
76+proc upsertFeed*(g: PulseboardDb, f: Feed) =
7777 ## Insert, or refresh the metadata of an existing feed.
7878 ##
7979 ## subscriber_count and the error columns are deliberately not touched: they
@@ -91,12 +91,12 @@ proc upsertFeed*(g: GleanDb, f: Feed) =
9191 p(f.feedUrl), pOrNull(f.title), pOrNull(f.siteUrl),
9292 pOrNull(f.description), pOrNull(f.feedType), pOrNull(f.faviconUrl))
9393
94-proc getFeed*(g: GleanDb, feedUrl: string): Option[Feed] =
94+proc getFeed*(g: PulseboardDb, feedUrl: string): Option[Feed] =
9595 g.db.queryFirst("SELECT " & feedColumns &
9696 " FROM articles.feeds f WHERE f.feed_url = ?",
9797 [p(feedUrl)], readFeed)
9898
99-proc getFeedsToFetch*(g: GleanDb, olderThan: Duration, limit: int): seq[Feed] =
99+proc getFeedsToFetch*(g: PulseboardDb, olderThan: Duration, limit: int): seq[Feed] =
100100 ## Feeds due for a refresh: never fetched, or last fetched before the
101101 ## cutoff. Dead feeds are excluded by error_count so a permanently broken
102102 ## URL does not consume a fetch slot every cycle.
@@ -110,7 +110,7 @@ proc getFeedsToFetch*(g: GleanDb, olderThan: Duration, limit: int): seq[Feed] =
110110 LIMIT ?""",
111111 [p(cutoff), p(limit.int64)], readFeed)
112112
113-proc markFeedFetched*(g: GleanDb, feedUrl: string) =
113+proc markFeedFetched*(g: PulseboardDb, feedUrl: string) =
114114 ## A successful fetch clears the error state, so a feed that recovers
115115 ## re-enters the rotation instead of staying dead.
116116 g.db.run("""UPDATE articles.feeds
@@ -119,14 +119,14 @@ proc markFeedFetched*(g: GleanDb, feedUrl: string) =
119119 error_count = 0
120120 WHERE feed_url = ?""", p(feedUrl))
121121
122-proc markFeedFetchError*(g: GleanDb, feedUrl, lastError: string) =
122+proc markFeedFetchError*(g: PulseboardDb, feedUrl, lastError: string) =
123123 g.db.run("""UPDATE articles.feeds
124124 SET last_fetched_at = CURRENT_TIMESTAMP,
125125 last_error = ?,
126126 error_count = error_count + 1
127127 WHERE feed_url = ?""", p(lastError), p(feedUrl))
128128
129-proc updateFeedFavicon*(g: GleanDb, feedUrl, faviconUrl: string) =
129+proc updateFeedFavicon*(g: PulseboardDb, feedUrl, faviconUrl: string) =
130130 g.db.run("UPDATE articles.feeds SET favicon_url = ? WHERE feed_url = ?",
131131 pOrNull(faviconUrl), p(feedUrl))
132132
@@ -146,7 +146,7 @@ proc readSubscription(r: Row): Subscription =
146146 faviconUrl: r.str(9),
147147 )
148148
149-proc createSubscription*(g: GleanDb, userDid, feedUrl, title, category,
149+proc createSubscription*(g: PulseboardDb, userDid, feedUrl, title, category,
150150 uri, cid: string) =
151151 ## Subscribe, and keep subscriber_count in step.
152152 ##
@@ -172,7 +172,7 @@ proc createSubscription*(g: GleanDb, userDid, feedUrl, title, category,
172172 WHERE feed_url = ?)
173173 WHERE feed_url = ?""", p(feedUrl), p(feedUrl))
174174
175-proc deleteSubscription*(g: GleanDb, userDid, feedUrl: string) =
175+proc deleteSubscription*(g: PulseboardDb, userDid, feedUrl: string) =
176176 g.db.transaction:
177177 g.db.run("""DELETE FROM articles.subscriptions
178178 WHERE user_did = ? AND feed_url = ?""", p(userDid), p(feedUrl))
@@ -182,14 +182,14 @@ proc deleteSubscription*(g: GleanDb, userDid, feedUrl: string) =
182182 WHERE feed_url = ?)
183183 WHERE feed_url = ?""", p(feedUrl), p(feedUrl))
184184
185-proc deleteAllSubscriptions*(g: GleanDb, userDid: string) =
185+proc deleteAllSubscriptions*(g: PulseboardDb, userDid: string) =
186186 g.db.transaction:
187187 g.db.run("DELETE FROM articles.subscriptions WHERE user_did = ?", p(userDid))
188188 g.db.run("""UPDATE articles.feeds SET subscriber_count = (
189189 SELECT count(*) FROM articles.subscriptions s
190190 WHERE s.feed_url = articles.feeds.feed_url)""")
191191
192-proc getSubscription*(g: GleanDb, userDid, feedUrl: string): Option[Subscription] =
192+proc getSubscription*(g: PulseboardDb, userDid, feedUrl: string): Option[Subscription] =
193193 g.db.queryFirst("""
194194 SELECT s.id, s.user_did, s.feed_url,
195195 COALESCE(s.title, f.title, s.feed_url), s.category, s.uri, s.cid,
@@ -199,7 +199,7 @@ proc getSubscription*(g: GleanDb, userDid, feedUrl: string): Option[Subscription
199199 WHERE s.user_did = ? AND s.feed_url = ?""",
200200 [p(userDid), p(feedUrl)], readSubscription)
201201
202-proc listSubscriptions*(g: GleanDb, userDid, category: string,
202+proc listSubscriptions*(g: PulseboardDb, userDid, category: string,
203203 limit, offset: int): seq[Subscription] =
204204 ## The unread count is computed per row here rather than stored: it is
205205 ## per-user and changes constantly, so a cached column would be wrong more
@@ -225,18 +225,18 @@ proc listSubscriptions*(g: GleanDb, userDid, category: string,
225225 params.add p(offset.int64)
226226 g.db.queryAll(sql, params, readSubscription)
227227
228-proc getSubscriptionCount*(g: GleanDb, userDid: string): int =
228+proc getSubscriptionCount*(g: PulseboardDb, userDid: string): int =
229229 g.db.queryInt("SELECT count(*) FROM articles.subscriptions WHERE user_did = ?",
230230 p(userDid)).get(0).int
231231
232-proc getCategories*(g: GleanDb, userDid: string): seq[string] =
232+proc getCategories*(g: PulseboardDb, userDid: string): seq[string] =
233233 for r in g.db.query("""SELECT DISTINCT category FROM articles.subscriptions
234234 WHERE user_did = ? AND category IS NOT NULL
235235 AND category != ''
236236 ORDER BY category COLLATE NOCASE""", p(userDid)):
237237 result.add r.str(0)
238238
239-proc listDeadFeeds*(g: GleanDb, userDid: string, threshold: int): seq[Feed] =
239+proc listDeadFeeds*(g: PulseboardDb, userDid: string, threshold: int): seq[Feed] =
240240 ## Subscribed feeds that keep failing, so the UI can offer a retry rather
241241 ## than silently showing nothing.
242242 g.db.queryAll("""SELECT """ & feedColumns & """
@@ -246,14 +246,14 @@ proc listDeadFeeds*(g: GleanDb, userDid: string, threshold: int): seq[Feed] =
246246 ORDER BY f.error_count DESC""",
247247 [p(userDid), p(threshold.int64)], readFeed)
248248
249-proc recountSubscriberCounts*(g: GleanDb) =
249+proc recountSubscriberCounts*(g: PulseboardDb) =
250250 ## Repair pass. The counts are maintained incrementally, so anything that
251251 ## interrupts a subscribe leaves them drifted.
252252 g.db.run("""UPDATE articles.feeds SET subscriber_count = (
253253 SELECT count(*) FROM articles.subscriptions s
254254 WHERE s.feed_url = articles.feeds.feed_url)""")
255255
256-proc batchUpsertFeeds*(g: GleanDb, feeds: seq[Feed]) =
256+proc batchUpsertFeeds*(g: PulseboardDb, feeds: seq[Feed]) =
257257 if feeds.len == 0: return
258258 g.db.transaction:
259259 var ps = g.db.prepared("""
@@ -6,7 +6,7 @@
6 ## read on every listing and written only when someone subscribes.6 ## read on every listing and written only when someone subscribes.
7 7
8 import std/[options, times]8 import std/[options, times]
9-import ./gleandb9+import ./pulseboarddb
10 import ./sqlite10 import ./sqlite
11 11
12 type12 type
@@ -73,7 +73,7 @@ const feedColumns = """f.feed_url, f.title, f.site_url, f.description,
73 f.feed_type, f.favicon_url, f.subscriber_count, f.error_count, f.last_error,73 f.feed_type, f.favicon_url, f.subscriber_count, f.error_count, f.last_error,
74 f.last_fetched_at"""74 f.last_fetched_at"""
75 75
76-proc upsertFeed*(g: GleanDb, f: Feed) =76+proc upsertFeed*(g: PulseboardDb, f: Feed) =
77 ## Insert, or refresh the metadata of an existing feed.77 ## Insert, or refresh the metadata of an existing feed.
78 ##78 ##
79 ## subscriber_count and the error columns are deliberately not touched: they79 ## subscriber_count and the error columns are deliberately not touched: they
@@ -91,12 +91,12 @@ proc upsertFeed*(g: GleanDb, f: Feed) =
91 p(f.feedUrl), pOrNull(f.title), pOrNull(f.siteUrl),91 p(f.feedUrl), pOrNull(f.title), pOrNull(f.siteUrl),
92 pOrNull(f.description), pOrNull(f.feedType), pOrNull(f.faviconUrl))92 pOrNull(f.description), pOrNull(f.feedType), pOrNull(f.faviconUrl))
93 93
94-proc getFeed*(g: GleanDb, feedUrl: string): Option[Feed] =94+proc getFeed*(g: PulseboardDb, feedUrl: string): Option[Feed] =
95 g.db.queryFirst("SELECT " & feedColumns &95 g.db.queryFirst("SELECT " & feedColumns &
96 " FROM articles.feeds f WHERE f.feed_url = ?",96 " FROM articles.feeds f WHERE f.feed_url = ?",
97 [p(feedUrl)], readFeed)97 [p(feedUrl)], readFeed)
98 98
99-proc getFeedsToFetch*(g: GleanDb, olderThan: Duration, limit: int): seq[Feed] =99+proc getFeedsToFetch*(g: PulseboardDb, olderThan: Duration, limit: int): seq[Feed] =
100 ## Feeds due for a refresh: never fetched, or last fetched before the100 ## Feeds due for a refresh: never fetched, or last fetched before the
101 ## cutoff. Dead feeds are excluded by error_count so a permanently broken101 ## cutoff. Dead feeds are excluded by error_count so a permanently broken
102 ## URL does not consume a fetch slot every cycle.102 ## URL does not consume a fetch slot every cycle.
@@ -110,7 +110,7 @@ proc getFeedsToFetch*(g: GleanDb, olderThan: Duration, limit: int): seq[Feed] =
110 LIMIT ?""",110 LIMIT ?""",
111 [p(cutoff), p(limit.int64)], readFeed)111 [p(cutoff), p(limit.int64)], readFeed)
112 112
113-proc markFeedFetched*(g: GleanDb, feedUrl: string) =113+proc markFeedFetched*(g: PulseboardDb, feedUrl: string) =
114 ## A successful fetch clears the error state, so a feed that recovers114 ## A successful fetch clears the error state, so a feed that recovers
115 ## re-enters the rotation instead of staying dead.115 ## re-enters the rotation instead of staying dead.
116 g.db.run("""UPDATE articles.feeds116 g.db.run("""UPDATE articles.feeds
@@ -119,14 +119,14 @@ proc markFeedFetched*(g: GleanDb, feedUrl: string) =
119 error_count = 0119 error_count = 0
120 WHERE feed_url = ?""", p(feedUrl))120 WHERE feed_url = ?""", p(feedUrl))
121 121
122-proc markFeedFetchError*(g: GleanDb, feedUrl, lastError: string) =122+proc markFeedFetchError*(g: PulseboardDb, feedUrl, lastError: string) =
123 g.db.run("""UPDATE articles.feeds123 g.db.run("""UPDATE articles.feeds
124 SET last_fetched_at = CURRENT_TIMESTAMP,124 SET last_fetched_at = CURRENT_TIMESTAMP,
125 last_error = ?,125 last_error = ?,
126 error_count = error_count + 1126 error_count = error_count + 1
127 WHERE feed_url = ?""", p(lastError), p(feedUrl))127 WHERE feed_url = ?""", p(lastError), p(feedUrl))
128 128
129-proc updateFeedFavicon*(g: GleanDb, feedUrl, faviconUrl: string) =129+proc updateFeedFavicon*(g: PulseboardDb, feedUrl, faviconUrl: string) =
130 g.db.run("UPDATE articles.feeds SET favicon_url = ? WHERE feed_url = ?",130 g.db.run("UPDATE articles.feeds SET favicon_url = ? WHERE feed_url = ?",
131 pOrNull(faviconUrl), p(feedUrl))131 pOrNull(faviconUrl), p(feedUrl))
132 132
@@ -146,7 +146,7 @@ proc readSubscription(r: Row): Subscription =
146 faviconUrl: r.str(9),146 faviconUrl: r.str(9),
147 )147 )
148 148
149-proc createSubscription*(g: GleanDb, userDid, feedUrl, title, category,149+proc createSubscription*(g: PulseboardDb, userDid, feedUrl, title, category,
150 uri, cid: string) =150 uri, cid: string) =
151 ## Subscribe, and keep subscriber_count in step.151 ## Subscribe, and keep subscriber_count in step.
152 ##152 ##
@@ -172,7 +172,7 @@ proc createSubscription*(g: GleanDb, userDid, feedUrl, title, category,
172 WHERE feed_url = ?)172 WHERE feed_url = ?)
173 WHERE feed_url = ?""", p(feedUrl), p(feedUrl))173 WHERE feed_url = ?""", p(feedUrl), p(feedUrl))
174 174
175-proc deleteSubscription*(g: GleanDb, userDid, feedUrl: string) =175+proc deleteSubscription*(g: PulseboardDb, userDid, feedUrl: string) =
176 g.db.transaction:176 g.db.transaction:
177 g.db.run("""DELETE FROM articles.subscriptions177 g.db.run("""DELETE FROM articles.subscriptions
178 WHERE user_did = ? AND feed_url = ?""", p(userDid), p(feedUrl))178 WHERE user_did = ? AND feed_url = ?""", p(userDid), p(feedUrl))
@@ -182,14 +182,14 @@ proc deleteSubscription*(g: GleanDb, userDid, feedUrl: string) =
182 WHERE feed_url = ?)182 WHERE feed_url = ?)
183 WHERE feed_url = ?""", p(feedUrl), p(feedUrl))183 WHERE feed_url = ?""", p(feedUrl), p(feedUrl))
184 184
185-proc deleteAllSubscriptions*(g: GleanDb, userDid: string) =185+proc deleteAllSubscriptions*(g: PulseboardDb, userDid: string) =
186 g.db.transaction:186 g.db.transaction:
187 g.db.run("DELETE FROM articles.subscriptions WHERE user_did = ?", p(userDid))187 g.db.run("DELETE FROM articles.subscriptions WHERE user_did = ?", p(userDid))
188 g.db.run("""UPDATE articles.feeds SET subscriber_count = (188 g.db.run("""UPDATE articles.feeds SET subscriber_count = (
189 SELECT count(*) FROM articles.subscriptions s189 SELECT count(*) FROM articles.subscriptions s
190 WHERE s.feed_url = articles.feeds.feed_url)""")190 WHERE s.feed_url = articles.feeds.feed_url)""")
191 191
192-proc getSubscription*(g: GleanDb, userDid, feedUrl: string): Option[Subscription] =192+proc getSubscription*(g: PulseboardDb, userDid, feedUrl: string): Option[Subscription] =
193 g.db.queryFirst("""193 g.db.queryFirst("""
194 SELECT s.id, s.user_did, s.feed_url,194 SELECT s.id, s.user_did, s.feed_url,
195 COALESCE(s.title, f.title, s.feed_url), s.category, s.uri, s.cid,195 COALESCE(s.title, f.title, s.feed_url), s.category, s.uri, s.cid,
@@ -199,7 +199,7 @@ proc getSubscription*(g: GleanDb, userDid, feedUrl: string): Option[Subscription
199 WHERE s.user_did = ? AND s.feed_url = ?""",199 WHERE s.user_did = ? AND s.feed_url = ?""",
200 [p(userDid), p(feedUrl)], readSubscription)200 [p(userDid), p(feedUrl)], readSubscription)
201 201
202-proc listSubscriptions*(g: GleanDb, userDid, category: string,202+proc listSubscriptions*(g: PulseboardDb, userDid, category: string,
203 limit, offset: int): seq[Subscription] =203 limit, offset: int): seq[Subscription] =
204 ## The unread count is computed per row here rather than stored: it is204 ## The unread count is computed per row here rather than stored: it is
205 ## per-user and changes constantly, so a cached column would be wrong more205 ## per-user and changes constantly, so a cached column would be wrong more
@@ -225,18 +225,18 @@ proc listSubscriptions*(g: GleanDb, userDid, category: string,
225 params.add p(offset.int64)225 params.add p(offset.int64)
226 g.db.queryAll(sql, params, readSubscription)226 g.db.queryAll(sql, params, readSubscription)
227 227
228-proc getSubscriptionCount*(g: GleanDb, userDid: string): int =228+proc getSubscriptionCount*(g: PulseboardDb, userDid: string): int =
229 g.db.queryInt("SELECT count(*) FROM articles.subscriptions WHERE user_did = ?",229 g.db.queryInt("SELECT count(*) FROM articles.subscriptions WHERE user_did = ?",
230 p(userDid)).get(0).int230 p(userDid)).get(0).int
231 231
232-proc getCategories*(g: GleanDb, userDid: string): seq[string] =232+proc getCategories*(g: PulseboardDb, userDid: string): seq[string] =
233 for r in g.db.query("""SELECT DISTINCT category FROM articles.subscriptions233 for r in g.db.query("""SELECT DISTINCT category FROM articles.subscriptions
234 WHERE user_did = ? AND category IS NOT NULL234 WHERE user_did = ? AND category IS NOT NULL
235 AND category != ''235 AND category != ''
236 ORDER BY category COLLATE NOCASE""", p(userDid)):236 ORDER BY category COLLATE NOCASE""", p(userDid)):
237 result.add r.str(0)237 result.add r.str(0)
238 238
239-proc listDeadFeeds*(g: GleanDb, userDid: string, threshold: int): seq[Feed] =239+proc listDeadFeeds*(g: PulseboardDb, userDid: string, threshold: int): seq[Feed] =
240 ## Subscribed feeds that keep failing, so the UI can offer a retry rather240 ## Subscribed feeds that keep failing, so the UI can offer a retry rather
241 ## than silently showing nothing.241 ## than silently showing nothing.
242 g.db.queryAll("""SELECT """ & feedColumns & """242 g.db.queryAll("""SELECT """ & feedColumns & """
@@ -246,14 +246,14 @@ proc listDeadFeeds*(g: GleanDb, userDid: string, threshold: int): seq[Feed] =
246 ORDER BY f.error_count DESC""",246 ORDER BY f.error_count DESC""",
247 [p(userDid), p(threshold.int64)], readFeed)247 [p(userDid), p(threshold.int64)], readFeed)
248 248
249-proc recountSubscriberCounts*(g: GleanDb) =249+proc recountSubscriberCounts*(g: PulseboardDb) =
250 ## Repair pass. The counts are maintained incrementally, so anything that250 ## Repair pass. The counts are maintained incrementally, so anything that
251 ## interrupts a subscribe leaves them drifted.251 ## interrupts a subscribe leaves them drifted.
252 g.db.run("""UPDATE articles.feeds SET subscriber_count = (252 g.db.run("""UPDATE articles.feeds SET subscriber_count = (
253 SELECT count(*) FROM articles.subscriptions s253 SELECT count(*) FROM articles.subscriptions s
254 WHERE s.feed_url = articles.feeds.feed_url)""")254 WHERE s.feed_url = articles.feeds.feed_url)""")
255 255
256-proc batchUpsertFeeds*(g: GleanDb, feeds: seq[Feed]) =256+proc batchUpsertFeeds*(g: PulseboardDb, feeds: seq[Feed]) =
257 if feeds.len == 0: return257 if feeds.len == 0: return
258 g.db.transaction:258 g.db.transaction:
259 var ps = g.db.prepared("""259 var ps = g.db.prepared("""
modified spike/nim/reader/opml.nim +1 -1
@@ -40,7 +40,7 @@ proc parseOpml*(source: string): seq[OpmlFeed] =
4040 raise newException(ValueError, "expected an OPML document")
4141 collect(root, "", result)
4242
43-proc renderOpml*(feeds: openArray[OpmlFeed], title = "Glean subscriptions"): string =
43+proc renderOpml*(feeds: openArray[OpmlFeed], title = "Pulseboard subscriptions"): string =
4444 result = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<opml version=\"2.0\">\n <head><title>" &
4545 escapeXml(title) & "</title></head>\n <body>\n"
4646 var uncategorized: seq[OpmlFeed]
@@ -40,7 +40,7 @@ proc parseOpml*(source: string): seq[OpmlFeed] =
40 raise newException(ValueError, "expected an OPML document")40 raise newException(ValueError, "expected an OPML document")
41 collect(root, "", result)41 collect(root, "", result)
42 42
43-proc renderOpml*(feeds: openArray[OpmlFeed], title = "Glean subscriptions"): string =43+proc renderOpml*(feeds: openArray[OpmlFeed], title = "Pulseboard subscriptions"): string =
44 result = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<opml version=\"2.0\">\n <head><title>" &44 result = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<opml version=\"2.0\">\n <head><title>" &
45 escapeXml(title) & "</title></head>\n <body>\n"45 escapeXml(title) & "</title></head>\n <body>\n"
46 var uncategorized: seq[OpmlFeed]46 var uncategorized: seq[OpmlFeed]
renamed spike/nim/reader/pulseboarddb.nim +7 -7
similarity index 89%
rename from spike/nim/reader/gleandb.nim
rename to spike/nim/reader/pulseboarddb.nim
@@ -15,7 +15,7 @@ import ./schema
1515 import ./sqlite
1616
1717 type
18- GleanDb* = object
18+ PulseboardDb* = object
1919 db*: Db
2020 basePath*: string
2121
@@ -51,7 +51,7 @@ proc applyPragmas(db: Db, schemaName: string, pragmas: openArray[string]) =
5151 except SqliteError:
5252 discard
5353
54-proc open*(basePath: string): GleanDb =
54+proc open*(basePath: string): PulseboardDb =
5555 ## Open `<basePath>_users` and attach `_articles`.
5656 let
5757 usersPath = basePath & "_users"
@@ -66,21 +66,21 @@ proc open*(basePath: string): GleanDb =
6666 db.exec &"ATTACH DATABASE '{path.replace(\"'\", \"''\")}' AS {name}"
6767 applyPragmas(db, name, AttachedPragmas)
6868
69- GleanDb(db: db, basePath: basePath)
69+ PulseboardDb(db: db, basePath: basePath)
7070
71-proc migrate*(g: GleanDb) =
71+proc migrate*(g: PulseboardDb) =
7272 ## Apply the schema. Every statement is IF NOT EXISTS, so this runs on every
7373 ## boot rather than being gated on a version.
7474 for stmt in readerSchema: g.db.exec stmt
7575
76-proc close*(g: var GleanDb) =
76+proc close*(g: var PulseboardDb) =
7777 g.db.close()
7878
79-proc journalMode*(g: GleanDb, schemaName = ""): string =
79+proc journalMode*(g: PulseboardDb, schemaName = ""): string =
8080 let sql = if schemaName.len == 0: "PRAGMA journal_mode"
8181 else: &"PRAGMA {schemaName}.journal_mode"
8282 g.db.queryText(sql).get("")
8383
84-proc attachedSchemas*(g: GleanDb): seq[string] =
84+proc attachedSchemas*(g: PulseboardDb): seq[string] =
8585 for row in g.db.rows("PRAGMA database_list"):
8686 result.add row[1]
similarity index 89%
rename from spike/nim/reader/gleandb.nim
rename to spike/nim/reader/pulseboarddb.nim
@@ -15,7 +15,7 @@ import ./schema
15 import ./sqlite15 import ./sqlite
16 16
17 type17 type
18- GleanDb* = object18+ PulseboardDb* = object
19 db*: Db19 db*: Db
20 basePath*: string20 basePath*: string
21 21
@@ -51,7 +51,7 @@ proc applyPragmas(db: Db, schemaName: string, pragmas: openArray[string]) =
51 except SqliteError:51 except SqliteError:
52 discard52 discard
53 53
54-proc open*(basePath: string): GleanDb =54+proc open*(basePath: string): PulseboardDb =
55 ## Open `<basePath>_users` and attach `_articles`.55 ## Open `<basePath>_users` and attach `_articles`.
56 let56 let
57 usersPath = basePath & "_users"57 usersPath = basePath & "_users"
@@ -66,21 +66,21 @@ proc open*(basePath: string): GleanDb =
66 db.exec &"ATTACH DATABASE '{path.replace(\"'\", \"''\")}' AS {name}"66 db.exec &"ATTACH DATABASE '{path.replace(\"'\", \"''\")}' AS {name}"
67 applyPragmas(db, name, AttachedPragmas)67 applyPragmas(db, name, AttachedPragmas)
68 68
69- GleanDb(db: db, basePath: basePath)69+ PulseboardDb(db: db, basePath: basePath)
70 70
71-proc migrate*(g: GleanDb) =71+proc migrate*(g: PulseboardDb) =
72 ## Apply the schema. Every statement is IF NOT EXISTS, so this runs on every72 ## Apply the schema. Every statement is IF NOT EXISTS, so this runs on every
73 ## boot rather than being gated on a version.73 ## boot rather than being gated on a version.
74 for stmt in readerSchema: g.db.exec stmt74 for stmt in readerSchema: g.db.exec stmt
75 75
76-proc close*(g: var GleanDb) =76+proc close*(g: var PulseboardDb) =
77 g.db.close()77 g.db.close()
78 78
79-proc journalMode*(g: GleanDb, schemaName = ""): string =79+proc journalMode*(g: PulseboardDb, schemaName = ""): string =
80 let sql = if schemaName.len == 0: "PRAGMA journal_mode"80 let sql = if schemaName.len == 0: "PRAGMA journal_mode"
81 else: &"PRAGMA {schemaName}.journal_mode"81 else: &"PRAGMA {schemaName}.journal_mode"
82 g.db.queryText(sql).get("")82 g.db.queryText(sql).get("")
83 83
84-proc attachedSchemas*(g: GleanDb): seq[string] =84+proc attachedSchemas*(g: PulseboardDb): seq[string] =
85 for row in g.db.rows("PRAGMA database_list"):85 for row in g.db.rows("PRAGMA database_list"):
86 result.add row[1]86 result.add row[1]
modified spike/nim/reader/sqlite.nim +1 -1
@@ -172,7 +172,7 @@ proc isNull*(r: Row, col: int): bool =
172172 sqlite3_column_type(r.stmt, col.cint) == SQLITE_NULL
173173
174174 proc str*(r: Row, col: int): string =
175- ## NULL reads as "". Most of Glean's text columns are nullable but mean the
175+ ## NULL reads as "". Most of Pulseboard's text columns are nullable but mean the
176176 ## empty string, and the Go side collapses them the same way at the wire
177177 ## boundary (see nullStr in internal/server/api.go).
178178 if r.isNull(col): return ""
@@ -172,7 +172,7 @@ proc isNull*(r: Row, col: int): bool =
172 sqlite3_column_type(r.stmt, col.cint) == SQLITE_NULL172 sqlite3_column_type(r.stmt, col.cint) == SQLITE_NULL
173 173
174 proc str*(r: Row, col: int): string =174 proc str*(r: Row, col: int): string =
175- ## NULL reads as "". Most of Glean's text columns are nullable but mean the175+ ## NULL reads as "". Most of Pulseboard's text columns are nullable but mean the
176 ## empty string, and the Go side collapses them the same way at the wire176 ## empty string, and the Go side collapses them the same way at the wire
177 ## boundary (see nullStr in internal/server/api.go).177 ## boundary (see nullStr in internal/server/api.go).
178 if r.isNull(col): return ""178 if r.isNull(col): return ""
modified spike/nim/readme.md +5 -5
@@ -1,14 +1,14 @@
1-# glean — a small RSS reader
1+# pulseboard — a small RSS reader
22
33 One binary, one SQLite database, no accounts.
44
55 ```
6-nim c -d:ssl glean.nim
7-./glean
6+nim c -d:ssl pulseboard.nim
7+./pulseboard
88 ```
99
1010 Then open <http://127.0.0.1:8080>. The database lives under
11-`~/.local/share/glean/`; override with `GLEAN_PORT` and `GLEAN_DB`.
11+`~/.local/share/pulseboard/`; override with `PULSEBOARD_PORT` and `PULSEBOARD_DB`.
1212
1313 ## The interface
1414
@@ -77,7 +77,7 @@ Six modules under `reader/`:
7777 | `feedparser` | RSS 2.0, RDF, Atom and JSON Feed |
7878 | `scraper` | readability-style extraction, output sanitised |
7979 | `sqlite` | a thin wrapper: prepare, bind, step, transactions |
80-| `gleandb` | two attached databases and the schema |
80+| `pulseboarddb` | two attached databases and the schema |
8181 | `feedstore` / `articlestore` | the queries |
8282
8383 Each carries its own checks:
@@ -1,14 +1,14 @@
1-# glean — a small RSS reader1+# pulseboard — a small RSS reader
2 2
3 One binary, one SQLite database, no accounts.3 One binary, one SQLite database, no accounts.
4 4
5 ```5 ```
6-nim c -d:ssl glean.nim6+nim c -d:ssl pulseboard.nim
7-./glean7+./pulseboard
8 ```8 ```
9 9
10 Then open <http://127.0.0.1:8080>. The database lives under10 Then open <http://127.0.0.1:8080>. The database lives under
11-`~/.local/share/glean/`; override with `GLEAN_PORT` and `GLEAN_DB`.11+`~/.local/share/pulseboard/`; override with `PULSEBOARD_PORT` and `PULSEBOARD_DB`.
12 12
13 ## The interface13 ## The interface
14 14
@@ -77,7 +77,7 @@ Six modules under `reader/`:
77 | `feedparser` | RSS 2.0, RDF, Atom and JSON Feed |77 | `feedparser` | RSS 2.0, RDF, Atom and JSON Feed |
78 | `scraper` | readability-style extraction, output sanitised |78 | `scraper` | readability-style extraction, output sanitised |
79 | `sqlite` | a thin wrapper: prepare, bind, step, transactions |79 | `sqlite` | a thin wrapper: prepare, bind, step, transactions |
80-| `gleandb` | two attached databases and the schema |80+| `pulseboarddb` | two attached databases and the schema |
81 | `feedstore` / `articlestore` | the queries |81 | `feedstore` / `articlestore` | the queries |
82 82
83 Each carries its own checks:83 Each carries its own checks:
modified spike/nim/scraper_probe.nim +1 -1
@@ -197,7 +197,7 @@ proc main() =
197197 block:
198198 try:
199199 let client = newHttpClient(timeout = 20_000,
200- userAgent = "glean-nim-spike/0.1")
200+ userAgent = "pulseboard-nim-spike/0.1")
201201 defer: client.close()
202202 let feed = parseFeed(client.getContent("https://blog.rust-lang.org/feed.xml"),
203203 "https://blog.rust-lang.org/feed.xml")
@@ -197,7 +197,7 @@ proc main() =
197 block:197 block:
198 try:198 try:
199 let client = newHttpClient(timeout = 20_000,199 let client = newHttpClient(timeout = 20_000,
200- userAgent = "glean-nim-spike/0.1")200+ userAgent = "pulseboard-nim-spike/0.1")
201 defer: client.close()201 defer: client.close()
202 let feed = parseFeed(client.getContent("https://blog.rust-lang.org/feed.xml"),202 let feed = parseFeed(client.getContent("https://blog.rust-lang.org/feed.xml"),
203 "https://blog.rust-lang.org/feed.xml")203 "https://blog.rust-lang.org/feed.xml")
modified spike/nim/stores_probe.nim +3 -3
@@ -7,7 +7,7 @@
77 ## returns other people's feeds, a NULL that sorts to the top.
88
99 import std/[options, os, strformat, strutils, times]
10-import reader/[articlestore, feedstore, gleandb, sqlite]
10+import reader/[articlestore, feedstore, pulseboarddb, sqlite]
1111
1212 var failures = 0
1313
@@ -32,11 +32,11 @@ proc main() =
3232 echo "Article / feed store probe"
3333 echo ""
3434
35- let base = getTempDir() / "glean_stores_probe"
35+ let base = getTempDir() / "pulseboard_stores_probe"
3636 cleanup(base)
3737 defer: cleanup(base)
3838
39- var g = gleandb.open(base)
39+ var g = pulseboarddb.open(base)
4040 defer: g.close()
4141 g.migrate()
4242
@@ -7,7 +7,7 @@
7 ## returns other people's feeds, a NULL that sorts to the top.7 ## returns other people's feeds, a NULL that sorts to the top.
8 8
9 import std/[options, os, strformat, strutils, times]9 import std/[options, os, strformat, strutils, times]
10-import reader/[articlestore, feedstore, gleandb, sqlite]10+import reader/[articlestore, feedstore, pulseboarddb, sqlite]
11 11
12 var failures = 012 var failures = 0
13 13
@@ -32,11 +32,11 @@ proc main() =
32 echo "Article / feed store probe"32 echo "Article / feed store probe"
33 echo ""33 echo ""
34 34
35- let base = getTempDir() / "glean_stores_probe"35+ let base = getTempDir() / "pulseboard_stores_probe"
36 cleanup(base)36 cleanup(base)
37 defer: cleanup(base)37 defer: cleanup(base)
38 38
39- var g = gleandb.open(base)39+ var g = pulseboarddb.open(base)
40 defer: g.close()40 defer: g.close()
41 g.migrate()41 g.migrate()
42 42