@dennisrongo/skills 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +45 -16
  2. package/package.json +1 -1
  3. package/skills/nextjs-app-router/SKILL.md +228 -175
  4. package/skills/nextjs-app-router/references/anti-patterns.md +163 -99
  5. package/skills/nextjs-app-router/references/folder-layout.md +79 -46
  6. package/skills/nextjs-app-router/references/good-patterns.md +233 -99
  7. package/skills/nextjs-app-router/references/templates/api-base.md +24 -21
  8. package/skills/nextjs-app-router/references/templates/auth-slice.md +82 -78
  9. package/skills/nextjs-app-router/references/templates/ci-and-hooks.md +58 -6
  10. package/skills/nextjs-app-router/references/templates/db-client.md +48 -0
  11. package/skills/nextjs-app-router/references/templates/env-and-utils.md +90 -23
  12. package/skills/nextjs-app-router/references/templates/feature-slice.md +110 -47
  13. package/skills/nextjs-app-router/references/templates/form-with-zod.md +23 -15
  14. package/skills/nextjs-app-router/references/templates/middleware.md +69 -59
  15. package/skills/nextjs-app-router/references/templates/next-config.md +4 -14
  16. package/skills/nextjs-app-router/references/templates/nextauth-config.md +178 -0
  17. package/skills/nextjs-app-router/references/templates/package.md +35 -5
  18. package/skills/nextjs-app-router/references/templates/prisma-schema.md +162 -0
  19. package/skills/nextjs-app-router/references/templates/providers-and-store.md +35 -21
  20. package/skills/nextjs-app-router/references/templates/root-layout.md +99 -20
  21. package/skills/nextjs-app-router/references/templates/route-group-layouts.md +46 -6
  22. package/skills/nextjs-app-router/references/templates/route-handler.md +168 -0
  23. package/skills/nextjs-app-router/references/templates/testing.md +105 -31
  24. package/skills/tauri-2-app/SKILL.md +381 -0
  25. package/skills/tauri-2-app/references/anti-patterns.md +434 -0
  26. package/skills/tauri-2-app/references/folder-layout.md +161 -0
  27. package/skills/tauri-2-app/references/good-patterns.md +477 -0
  28. package/skills/tauri-2-app/references/templates/build-rs.md +51 -0
  29. package/skills/tauri-2-app/references/templates/capabilities.md +68 -0
  30. package/skills/tauri-2-app/references/templates/cargo-toml.md +118 -0
  31. package/skills/tauri-2-app/references/templates/ci-workflow.md +99 -0
  32. package/skills/tauri-2-app/references/templates/encryption-mod.md +228 -0
  33. package/skills/tauri-2-app/references/templates/error-mod.md +126 -0
  34. package/skills/tauri-2-app/references/templates/lib-rs.md +98 -0
  35. package/skills/tauri-2-app/references/templates/macos-plist.md +89 -0
  36. package/skills/tauri-2-app/references/templates/main-rs.md +21 -0
  37. package/skills/tauri-2-app/references/templates/platform-traits.md +217 -0
  38. package/skills/tauri-2-app/references/templates/storage-mod.md +172 -0
  39. package/skills/tauri-2-app/references/templates/tauri-conf.md +136 -0
  40. package/skills/tauri-2-app/references/templates/use-tauri-command.md +194 -0
  41. package/skills/tauri-2-app/references/templates/vite-and-tsconfig.md +189 -0
@@ -0,0 +1,89 @@
1
+ # macOS `Info.plist` and `entitlements.plist`
2
+
3
+ Both files live at `src-tauri/Info.plist` and `src-tauri/entitlements.plist` and are referenced from `tauri.conf.json` `bundle.macOS`.
4
+
5
+ ## `Info.plist`
6
+
7
+ ```xml
8
+ <?xml version="1.0" encoding="UTF-8"?>
9
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
10
+ <plist version="1.0">
11
+ <dict>
12
+ <key>CFBundleIdentifier</key>
13
+ <string>{{bundle.identifier}}</string>
14
+
15
+ <!-- Usage descriptions: REQUIRED for every OS permission the app requests. -->
16
+ <!-- Without these, the permission prompt never shows and the call returns "denied". -->
17
+
18
+ <!-- Microphone — required if the app uses cpal / AVFoundation audio input. -->
19
+ <!-- <key>NSMicrophoneUsageDescription</key>
20
+ <string>{{ProductName}} needs the microphone to record voice input.</string> -->
21
+
22
+ <!-- Accessibility — required if the app sends synthetic keystrokes / reads UI. -->
23
+ <!-- <key>NSAccessibilityUsageDescription</key>
24
+ <string>{{ProductName}} needs Accessibility access to insert text into other apps.</string> -->
25
+
26
+ <!-- AppleEvents — required if the app sends AppleScript to other apps. -->
27
+ <!-- <key>NSAppleEventsUsageDescription</key>
28
+ <string>{{ProductName}} needs AppleEvents to control other applications.</string> -->
29
+
30
+ <!-- Camera, Location, Contacts, Calendar, Photos, etc. — add as needed. -->
31
+ </dict>
32
+ </plist>
33
+ ```
34
+
35
+ **Uncomment only the permissions the app actually uses.** Apple's notarization process flags apps that request permissions they don't exercise.
36
+
37
+ ## `entitlements.plist`
38
+
39
+ ```xml
40
+ <?xml version="1.0" encoding="UTF-8"?>
41
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
42
+ <plist version="1.0">
43
+ <dict>
44
+ <!-- Start with an EMPTY dict and add entitlements only when needed. -->
45
+ <!-- Each entitlement weakens the hardened runtime; the goal is minimum. -->
46
+
47
+ <!-- Audio input — required for microphone capture under sandbox/hardened runtime. -->
48
+ <!-- <key>com.apple.security.device.audio-input</key>
49
+ <true/> -->
50
+
51
+ <!-- AppleEvents — only if Info.plist also has NSAppleEventsUsageDescription. -->
52
+ <!-- <key>com.apple.security.automation.apple-events</key>
53
+ <true/> -->
54
+
55
+ <!-- JIT — only if the app embeds a JIT VM (rare; e.g. some ML runtimes). -->
56
+ <!-- <key>com.apple.security.cs.allow-jit</key>
57
+ <true/> -->
58
+
59
+ <!-- Unsigned executable memory — needed by some unsigned native libraries. -->
60
+ <!-- AVOID if possible; it weakens code-signing protections. -->
61
+ <!-- <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
62
+ <true/> -->
63
+ </dict>
64
+ </plist>
65
+ ```
66
+
67
+ ## How to know which entitlements you need
68
+
69
+ Don't guess. Build the app, run it, and watch Console.app for messages like:
70
+
71
+ ```
72
+ {{ProductName}} (12345) deny(1) device-microphone
73
+ ```
74
+
75
+ That tells you the missing entitlement — in this case, `com.apple.security.device.audio-input`. Add it, rebuild, retest.
76
+
77
+ For network entitlements (`com.apple.security.network.client`, `com.apple.security.network.server`), only enable if running under the App Store sandbox. The hardened runtime by itself does not block network access.
78
+
79
+ ## Why minimal entitlements matter
80
+
81
+ - **Notarization**: Apple's notarization service reviews entitlement lists. Over-broad entitlements (`com.apple.security.cs.disable-library-validation`, `com.apple.security.cs.allow-dyld-environment-variables`) may delay or block notarization.
82
+ - **App Store**: stricter still. Only specific entitlements are allowed for App Store apps.
83
+ - **Security audit surface**: if a CVE is found in a framework you don't use, an unused entitlement does not save you; if you weren't using `com.apple.security.cs.allow-jit`, you're not affected by JIT-related issues. Tight entitlements = tight blast radius.
84
+
85
+ ## Why minimal usage descriptions matter
86
+
87
+ - Listing `NSCameraUsageDescription` when the app never accesses the camera misleads users about what the app does, and macOS may still prompt for the permission inappropriately.
88
+ - App Store reviewers reject apps that request permissions they don't use.
89
+ - The strings appear in System Settings → Privacy → Microphone (etc.). Make them descriptive ("X needs the microphone to transcribe voice notes" is better than "X needs microphone access").
@@ -0,0 +1,21 @@
1
+ # `src-tauri/src/main.rs`
2
+
3
+ The tiny shim that calls into `lib.rs`. Keep it short — all logic lives in `lib.rs`.
4
+
5
+ ```rust
6
+ // Prevents additional console window on Windows in release. DO NOT REMOVE.
7
+ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
8
+
9
+ #[cfg(target_os = "macos")]
10
+ embed_plist::embed_info_plist!("../Info.plist");
11
+
12
+ fn main() {
13
+ {{app_name}}_lib::run()
14
+ }
15
+ ```
16
+
17
+ ## Why
18
+
19
+ - `#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]` — Without this attribute, Windows release builds open a console window behind the Tauri window. Debug builds keep their console for `println!`.
20
+ - `embed_plist::embed_info_plist!` — Ensures macOS standalone binaries (run directly, not through the `.app` bundle) still carry the plist. Gated to macOS via `#[cfg(target_os = "macos")]`.
21
+ - `{{app_name}}_lib::run()` — The `_lib` suffix on the crate name avoids a Windows linker conflict between the bin and lib targets. The actual function is defined in `lib.rs`.
@@ -0,0 +1,217 @@
1
+ # `src-tauri/src/platform/` — Trait-based platform abstractions
2
+
3
+ Pattern: define traits in `platform/traits.rs`, implement per-OS in `macos.rs|windows.rs|linux.rs` gated with `#[cfg(target_os = "...")]`, expose `Send + Sync` wrappers via `wrappers.rs` for Tauri State. Commands receive `State<'_, PlatformXxx>` — they never see `cfg!`.
4
+
5
+ ## `platform/traits.rs`
6
+
7
+ ```rust
8
+ //! Platform trait contracts. Implementations live in `macos.rs`, `windows.rs`,
9
+ //! `linux.rs` and are gated with `#[cfg(target_os = "...")]`.
10
+
11
+ use std::path::Path;
12
+
13
+ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
14
+ pub struct PermissionStatus {
15
+ pub is_macos: bool,
16
+ pub accessibility: bool,
17
+ pub microphone: bool,
18
+ pub notification: bool,
19
+ pub all_granted: bool,
20
+ }
21
+
22
+ #[derive(Debug, thiserror::Error)]
23
+ pub enum PlatformError {
24
+ #[error("command execution error: {0}")]
25
+ CommandError(String),
26
+ #[error("unsupported on this platform")]
27
+ Unsupported,
28
+ }
29
+
30
+ /// Check OS-level permissions (mic, accessibility, notifications).
31
+ pub trait PermissionChecker: Send + Sync {
32
+ fn check_accessibility(&self) -> bool;
33
+ fn check_microphone(&self) -> bool;
34
+ fn check_notification(&self) -> bool;
35
+ fn get_status(&self) -> PermissionStatus;
36
+ fn open_system_settings(&self, permission_type: &str) -> Result<(), String>;
37
+ }
38
+
39
+ /// Open files/folders in OS-default apps.
40
+ pub trait FileOpener: Send + Sync {
41
+ fn open_in_default_app(&self, path: &Path) -> Result<(), String>;
42
+ fn open_in_file_manager(&self, path: &Path) -> Result<(), String>;
43
+ fn open_in_text_editor(&self, path: &Path) -> Result<(), String>;
44
+ }
45
+
46
+ #[cfg(test)]
47
+ mod tests {
48
+ use super::*;
49
+
50
+ #[test]
51
+ fn permission_status_round_trips() {
52
+ let s = PermissionStatus {
53
+ is_macos: true, accessibility: true, microphone: false,
54
+ notification: true, all_granted: false,
55
+ };
56
+ let json = serde_json::to_string(&s).unwrap();
57
+ let back: PermissionStatus = serde_json::from_str(&json).unwrap();
58
+ assert_eq!(s.microphone, back.microphone);
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## `platform/macos.rs` (sketch)
64
+
65
+ ```rust
66
+ #![cfg(target_os = "macos")]
67
+
68
+ use std::path::Path;
69
+ use std::process::Command;
70
+ use crate::platform::traits::{FileOpener, PermissionChecker, PermissionStatus};
71
+
72
+ pub struct MacOsPlatform;
73
+
74
+ impl PermissionChecker for MacOsPlatform {
75
+ fn check_accessibility(&self) -> bool {
76
+ // Use AXIsProcessTrusted via security-framework or objc bindings.
77
+ // For brevity, this stub assumes a helper exists.
78
+ unimplemented!("call AXIsProcessTrusted")
79
+ }
80
+ fn check_microphone(&self) -> bool {
81
+ unimplemented!("call AVCaptureDevice.authorizationStatus")
82
+ }
83
+ fn check_notification(&self) -> bool { true }
84
+ fn get_status(&self) -> PermissionStatus {
85
+ PermissionStatus {
86
+ is_macos: true,
87
+ accessibility: self.check_accessibility(),
88
+ microphone: self.check_microphone(),
89
+ notification: self.check_notification(),
90
+ all_granted: false, // compute from above
91
+ }
92
+ }
93
+ fn open_system_settings(&self, kind: &str) -> Result<(), String> {
94
+ let url = match kind {
95
+ "accessibility" => "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility",
96
+ "microphone" => "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone",
97
+ _ => return Err(format!("unknown permission: {}", kind)),
98
+ };
99
+ Command::new("open").arg(url).spawn()
100
+ .map(|_| ()).map_err(|e| e.to_string())
101
+ }
102
+ }
103
+
104
+ impl FileOpener for MacOsPlatform {
105
+ fn open_in_default_app(&self, path: &Path) -> Result<(), String> {
106
+ Command::new("open").arg(path).spawn().map(|_| ()).map_err(|e| e.to_string())
107
+ }
108
+ fn open_in_file_manager(&self, path: &Path) -> Result<(), String> {
109
+ Command::new("open").arg("-R").arg(path).spawn().map(|_| ()).map_err(|e| e.to_string())
110
+ }
111
+ fn open_in_text_editor(&self, path: &Path) -> Result<(), String> {
112
+ Command::new("open").arg("-t").arg(path).spawn().map(|_| ()).map_err(|e| e.to_string())
113
+ }
114
+ }
115
+ ```
116
+
117
+ `platform/windows.rs` and `platform/linux.rs` follow the same shape with `Command::new("explorer")` / `Command::new("xdg-open")` etc.
118
+
119
+ ## `platform/wrappers.rs`
120
+
121
+ Wrappers exist so Tauri's `State<T>` (which requires `T: Send + Sync`) can hold the platform impl regardless of OS.
122
+
123
+ ```rust
124
+ use crate::platform::traits::{FileOpener, PermissionChecker, PermissionStatus};
125
+ use std::path::Path;
126
+
127
+ pub struct PlatformPermissionChecker {
128
+ #[cfg(target_os = "macos")] inner: super::MacOsPlatform,
129
+ #[cfg(target_os = "windows")] inner: super::WindowsPlatform,
130
+ #[cfg(target_os = "linux")] inner: super::LinuxPlatform,
131
+ }
132
+
133
+ impl PlatformPermissionChecker {
134
+ pub fn new() -> Self {
135
+ Self {
136
+ #[cfg(target_os = "macos")] inner: super::MacOsPlatform,
137
+ #[cfg(target_os = "windows")] inner: super::WindowsPlatform,
138
+ #[cfg(target_os = "linux")] inner: super::LinuxPlatform,
139
+ }
140
+ }
141
+ }
142
+
143
+ impl PermissionChecker for PlatformPermissionChecker {
144
+ fn check_accessibility(&self) -> bool { self.inner.check_accessibility() }
145
+ fn check_microphone(&self) -> bool { self.inner.check_microphone() }
146
+ fn check_notification(&self) -> bool { self.inner.check_notification() }
147
+ fn get_status(&self) -> PermissionStatus { self.inner.get_status() }
148
+ fn open_system_settings(&self, k: &str) -> Result<(), String> {
149
+ self.inner.open_system_settings(k)
150
+ }
151
+ }
152
+
153
+ pub struct PlatformFileOpener { /* same shape */ }
154
+ // ... impl FileOpener for PlatformFileOpener ...
155
+ ```
156
+
157
+ ## `platform/mod.rs`
158
+
159
+ ```rust
160
+ pub mod traits;
161
+ pub mod wrappers;
162
+
163
+ #[cfg(target_os = "macos")] mod macos;
164
+ #[cfg(target_os = "windows")] mod windows;
165
+ #[cfg(target_os = "linux")] mod linux;
166
+
167
+ #[cfg(target_os = "macos")] pub use macos::MacOsPlatform;
168
+ #[cfg(target_os = "windows")] pub use windows::WindowsPlatform;
169
+ #[cfg(target_os = "linux")] pub use linux::LinuxPlatform;
170
+
171
+ pub use traits::{PermissionChecker, PermissionStatus, FileOpener};
172
+ pub use wrappers::{PlatformPermissionChecker, PlatformFileOpener};
173
+
174
+ #[cfg(test)]
175
+ mod tests {
176
+ use super::*;
177
+
178
+ #[test]
179
+ fn wrappers_are_send_sync() {
180
+ fn assert_send_sync<T: Send + Sync>() {}
181
+ assert_send_sync::<PlatformPermissionChecker>();
182
+ assert_send_sync::<PlatformFileOpener>();
183
+ }
184
+ }
185
+ ```
186
+
187
+ ## Usage in `lib.rs`
188
+
189
+ ```rust
190
+ .setup(|app| {
191
+ app.manage(PlatformPermissionChecker::new());
192
+ app.manage(PlatformFileOpener::new());
193
+ Ok(())
194
+ })
195
+ ```
196
+
197
+ ## Usage in commands
198
+
199
+ ```rust
200
+ #[tauri::command]
201
+ pub fn open_in_finder(
202
+ opener: tauri::State<'_, PlatformFileOpener>,
203
+ path: String,
204
+ ) -> Result<(), String> {
205
+ use crate::platform::FileOpener;
206
+ opener.open_in_file_manager(std::path::Path::new(&path))
207
+ }
208
+ ```
209
+
210
+ The command is **portable** — no `cfg!` checks, no `#[cfg(target_os = "...")]` attributes, no platform-specific imports.
211
+
212
+ ## Why this is worth the structural overhead
213
+
214
+ - **Adding a fourth OS** (BSD, mobile) is one new `platform/<os>.rs` file plus a `#[cfg]` arm in the wrapper. Commands don't change.
215
+ - **Mocking in tests** is trivial — define a `MockPermissionChecker` that implements `PermissionChecker`, register it in the test setup via `app.manage(...)`.
216
+ - **Audit surface** — all OS-specific code is in `platform/<os>.rs`. A security review of "what does the macOS build do differently?" reads one file.
217
+ - **Build times** — Cargo skips compiling `platform/windows.rs` on macOS targets entirely (the `#[cfg]` gate). No wasted work.
@@ -0,0 +1,172 @@
1
+ # `src-tauri/src/storage/mod.rs`
2
+
3
+ Shared JSON-on-disk utilities used by `settings/`, `history/`, `error_log/`, and any other module that persists structured data. **Every persistent feature uses these primitives** — never raw `std::fs` against `app_data_dir`.
4
+
5
+ ```rust
6
+ //! Shared storage utilities: paths, JSON I/O, IDs, timestamps, pruning.
7
+
8
+ use std::fs;
9
+ use std::path::PathBuf;
10
+ use chrono::Utc;
11
+ use serde::{de::DeserializeOwned, Serialize};
12
+ use tauri::{AppHandle, Manager};
13
+
14
+ /// Resolve and create the app data directory.
15
+ pub fn get_app_data_dir(app: &AppHandle) -> Result<PathBuf, String> {
16
+ let dir = app
17
+ .path()
18
+ .app_data_dir()
19
+ .map_err(|e| format!("Failed to get app data directory: {}", e))?;
20
+ fs::create_dir_all(&dir)
21
+ .map_err(|e| format!("Failed to create app data directory: {}", e))?;
22
+ Ok(dir)
23
+ }
24
+
25
+ /// Resolve a path inside the app data directory.
26
+ pub fn get_storage_path(app: &AppHandle, filename: &str) -> Result<PathBuf, String> {
27
+ Ok(get_app_data_dir(app)?.join(filename))
28
+ }
29
+
30
+ /// Load a `Vec<T>` from a JSON file. Returns empty on missing / invalid file
31
+ /// (logged at info / error level).
32
+ pub fn load_json<T: DeserializeOwned>(app: &AppHandle, filename: &str) -> Vec<T> {
33
+ let path = match get_storage_path(app, filename) {
34
+ Ok(p) => p,
35
+ Err(e) => {
36
+ tracing::error!("Storage path error for {}: {}", filename, e);
37
+ return Vec::new();
38
+ }
39
+ };
40
+
41
+ if !path.exists() {
42
+ tracing::info!("Storage file {} not found, starting empty", filename);
43
+ return Vec::new();
44
+ }
45
+
46
+ let content = match fs::read_to_string(&path) {
47
+ Ok(c) => c,
48
+ Err(e) => {
49
+ tracing::error!("Failed to read {}: {}", filename, e);
50
+ return Vec::new();
51
+ }
52
+ };
53
+
54
+ match serde_json::from_str::<Vec<T>>(&content) {
55
+ Ok(entries) => {
56
+ tracing::info!("Loaded {} entries from {}", entries.len(), filename);
57
+ entries
58
+ }
59
+ Err(e) => {
60
+ tracing::error!("Failed to parse JSON for {}: {}", filename, e);
61
+ Vec::new()
62
+ }
63
+ }
64
+ }
65
+
66
+ /// Save a slice as pretty-printed JSON to `filename` inside the app data dir.
67
+ pub fn save_json<T: Serialize>(
68
+ app: &AppHandle,
69
+ filename: &str,
70
+ entries: &[T],
71
+ ) -> Result<(), String> {
72
+ let path = get_storage_path(app, filename)?;
73
+ let json = serde_json::to_string_pretty(entries)
74
+ .map_err(|e| format!("Failed to serialize: {}", e))?;
75
+ fs::write(&path, json).map_err(|e| format!("Failed to write {}: {}", filename, e))?;
76
+ tracing::info!("Saved {} entries to {}", entries.len(), path.display());
77
+ Ok(())
78
+ }
79
+
80
+ /// Generate a unique ID from nanoseconds since epoch.
81
+ pub fn generate_id() -> String {
82
+ format!("{:x}", Utc::now().timestamp_nanos_opt().unwrap_or(0))
83
+ }
84
+
85
+ /// Generate a unique ID with a prefix.
86
+ pub fn generate_id_with_prefix(prefix: &str) -> String {
87
+ format!("{}{:x}", prefix, Utc::now().timestamp_nanos_opt().unwrap_or(0))
88
+ }
89
+
90
+ /// Current ISO 8601 timestamp (UTC).
91
+ pub fn get_timestamp() -> String {
92
+ Utc::now().to_rfc3339()
93
+ }
94
+
95
+ /// Parse an RFC 3339 timestamp to seconds since epoch.
96
+ pub fn parse_timestamp_to_seconds(s: &str) -> Option<u64> {
97
+ chrono::DateTime::parse_from_rfc3339(s)
98
+ .ok()
99
+ .map(|dt| dt.timestamp() as u64)
100
+ }
101
+
102
+ /// Trait for entries that carry a timestamp string.
103
+ pub trait Timestamped {
104
+ fn timestamp(&self) -> &str;
105
+ }
106
+
107
+ /// Drop entries older than `days_to_keep` days. Unparseable timestamps are kept.
108
+ pub fn prune_entries_by_age<T: Timestamped>(entries: Vec<T>, days_to_keep: u64) -> Vec<T> {
109
+ let cutoff = Utc::now().timestamp() as u64 - (days_to_keep * 24 * 3600);
110
+ entries
111
+ .into_iter()
112
+ .filter(|e| match parse_timestamp_to_seconds(e.timestamp()) {
113
+ Some(s) => s > cutoff,
114
+ None => {
115
+ tracing::warn!("Unparseable timestamp '{}', keeping entry", e.timestamp());
116
+ true
117
+ }
118
+ })
119
+ .collect()
120
+ }
121
+
122
+ #[cfg(test)]
123
+ mod tests {
124
+ use super::*;
125
+
126
+ #[test]
127
+ fn ids_are_unique_within_a_nanosecond_resolution() {
128
+ let a = generate_id();
129
+ std::thread::sleep(std::time::Duration::from_nanos(1));
130
+ let b = generate_id();
131
+ assert_ne!(a, b);
132
+ }
133
+
134
+ #[test]
135
+ fn timestamp_is_rfc3339() {
136
+ let ts = get_timestamp();
137
+ assert!(chrono::DateTime::parse_from_rfc3339(&ts).is_ok());
138
+ }
139
+
140
+ #[test]
141
+ fn prune_drops_old_entries() {
142
+ struct E(String);
143
+ impl Timestamped for E {
144
+ fn timestamp(&self) -> &str { &self.0 }
145
+ }
146
+ let now = Utc::now();
147
+ let old = now - chrono::Duration::days(10);
148
+ let new = now - chrono::Duration::days(1);
149
+ let entries = vec![
150
+ E(old.to_rfc3339()),
151
+ E(new.to_rfc3339()),
152
+ ];
153
+ let kept = prune_entries_by_age(entries, 5);
154
+ assert_eq!(kept.len(), 1);
155
+ }
156
+ }
157
+ ```
158
+
159
+ ## Why use `chrono`
160
+
161
+ The "hand-rolled date math" pattern (`seconds_since_epoch / 86400`, manual leap-year tables, manual month-length tables) is a classic anti-pattern. It looks small but every special case is a future bug — DST, leap seconds, century leap years, year-2038 on 32-bit platforms. `chrono` is ~200KB compressed, well-tested, and used everywhere. Skip the temptation to "save a dep".
162
+
163
+ ## Error handling philosophy
164
+
165
+ `load_json` returns `Vec<T>` rather than `Result<Vec<T>, _>`. Reasoning:
166
+ - Missing files are normal (first run).
167
+ - Parse errors are rare but should not crash the app — log and start fresh.
168
+ - Callers always want to keep going with an empty list rather than abort.
169
+
170
+ `save_json` returns `Result<_, String>` because a failed save is a real user-visible problem the UI should surface.
171
+
172
+ If a domain genuinely needs `Result<Vec<T>, _>` semantics on load (e.g. settings that can't safely default), wrap this primitive in a domain-specific function that does the strictness check.
@@ -0,0 +1,136 @@
1
+ # `src-tauri/tauri.conf.json`
2
+
3
+ ```json
4
+ {
5
+ "$schema": "https://schema.tauri.app/config/2",
6
+ "productName": "{{ProductName}}",
7
+ "version": "0.1.0",
8
+ "identifier": "{{bundle.identifier}}",
9
+ "build": {
10
+ "beforeDevCommand": "npm run dev",
11
+ "devUrl": "http://localhost:5173",
12
+ "beforeBuildCommand": "npm run build",
13
+ "frontendDist": "../dist"
14
+ },
15
+ "app": {
16
+ "security": {
17
+ "csp": null
18
+ },
19
+ "windows": [
20
+ {
21
+ "label": "main",
22
+ "title": "{{ProductName}}",
23
+ "width": 1000,
24
+ "height": 700,
25
+ "minWidth": 600,
26
+ "minHeight": 400,
27
+ "center": true,
28
+ "visible": true,
29
+ "dragDropEnabled": true
30
+ }
31
+ ]
32
+ },
33
+ "bundle": {
34
+ "active": true,
35
+ "targets": "all",
36
+ "icon": [
37
+ "icons/32x32.png",
38
+ "icons/128x128.png",
39
+ "icons/128x128@2x.png",
40
+ "icons/icon.icns",
41
+ "icons/icon.ico"
42
+ ],
43
+ "macOS": {
44
+ "infoPlist": "./Info.plist",
45
+ "entitlements": "./entitlements.plist",
46
+ "minimumSystemVersion": "10.15",
47
+ "hardenedRuntime": true
48
+ },
49
+ "windows": {
50
+ "nsis": {
51
+ "displayLanguageSelector": false,
52
+ "installMode": "perMachine",
53
+ "compression": "lzma"
54
+ },
55
+ "webviewInstallMode": {
56
+ "type": "embedBootstrapper"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## When the updater is enabled
64
+
65
+ Add (only if the user has generated a signing key with `npx tauri signer generate` AND has a manifest URL):
66
+
67
+ ```json
68
+ "bundle": {
69
+ // ... other fields
70
+ "createUpdaterArtifacts": true
71
+ },
72
+ "plugins": {
73
+ "updater": {
74
+ "active": true,
75
+ "endpoints": ["{{updater_manifest_url}}"],
76
+ "dialog": false,
77
+ "pubkey": "{{tauri_signing_pubkey}}"
78
+ }
79
+ }
80
+ ```
81
+
82
+ **Never** invent an `endpoints` URL or a `pubkey`. Both come from the user. The pubkey is a base64-encoded minisign public key produced by `tauri signer generate`. The endpoint is a JSON manifest the user controls (typical shape below).
83
+
84
+ ## Updater manifest shape (the URL points at this)
85
+
86
+ ```json
87
+ {
88
+ "version": "1.0.0",
89
+ "notes": "Initial release",
90
+ "pub_date": "2026-01-15T10:30:00Z",
91
+ "platforms": {
92
+ "windows-x86_64": {
93
+ "signature": "{{full base64 contents of .sig file}}",
94
+ "url": "{{cdn-url}}/{{ProductName}}_1.0.0_x64-setup.exe"
95
+ },
96
+ "darwin-aarch64": {
97
+ "signature": "{{full base64 contents of .sig file}}",
98
+ "url": "{{cdn-url}}/{{ProductName}}_1.0.0_aarch64.dmg"
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ ## Why these defaults
105
+
106
+ - **`identifier`** is reverse-DNS (e.g. `com.example.myapp`) and **cannot change** after release — it's the OS-level identity for keychain entries, settings paths, notification permissions, and update channels. Always ask the user.
107
+ - **`csp: null`** is the Tauri default and works for many apps. If the WebView loads remote content, set an explicit CSP whitelisting `tauri://localhost` plus any trusted origins.
108
+ - **`windows[].devtools` is omitted** — Tauri's default is off in release. Adding `"devtools": true` ships an inspect-element surface to every user.
109
+ - **`bundle.targets: "all"`** lets `tauri build` pick the right installer format for the host OS (DMG on macOS, NSIS on Windows, AppImage/deb on Linux).
110
+ - **`macOS.hardenedRuntime: true`** is required for notarization. Pair with a minimal `entitlements.plist` listing **only** what the app uses.
111
+ - **`nsis.installMode: "perMachine"`** installs for all users on Windows. Use `"currentUser"` for per-user installs (no admin prompt, but registry keys live under `HKCU`).
112
+ - **`webviewInstallMode: "embedBootstrapper"`** ships the WebView2 bootstrapper inside the installer so users on stripped Windows builds without WebView2 can still install.
113
+
114
+ ## Local config overlay
115
+
116
+ Tauri supports loading additional config files. Useful for local development with the updater disabled:
117
+
118
+ ```bash
119
+ # src-tauri/tauri.local-no-updater.conf.json
120
+ {
121
+ "$schema": "https://schema.tauri.app/config/2",
122
+ "plugins": {
123
+ "updater": {
124
+ "active": false
125
+ }
126
+ }
127
+ }
128
+ ```
129
+
130
+ Then build with:
131
+
132
+ ```bash
133
+ npx @tauri-apps/cli build --config tauri.local-no-updater.conf.json
134
+ ```
135
+
136
+ This avoids needing signing keys on every developer's machine.