commit 88488fc67623eea19c2a8b34646e28b1638b1dff
parent f1e753b56e70e4a303911ab14abb6aadbf7b5f11
Author: triesap <triesap@radroots.dev>
Date: Sun, 21 Dec 2025 01:37:29 +0000
Standardized code directives, added media configuration, refreshed dependencies, optimized localization access, enabled dynamic titles, revamped profile photo uploads.
Diffstat:
11 files changed, 167 insertions(+), 85 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -1,30 +1,38 @@
-# Rad Roots - Agricultural Network Application
-
-#### Code Rules
+# Rad Roots - Code Directives
1. TypeScript
-- Use strict TypeScript at all times.
-- Never use as any, any, unknown as, or any weakening of types.
+- Use strict TypeScript.
+- Do not use any, unknown casts, or weaken types.
- Prefer explicit interfaces and types.
-- Use enums or literal unions when appropriate.
-- Avoid default exports unless legacy structure requires it.
+- Use enums or literal unions when clear.
+- Use named exports; avoid default exports.
+
+2. Naming
+- Variables and functions use snake_case.
+- Types, interfaces, and enums use PascalCase.
+- Prefer layered prefixes to namespace meaning: domain_object_action, e.g. nostr_keys_gen(), NostrKeysGen.
+- Do not enforce naming conventions in _env*.ts files.
+- Constant data structures must use ALL_CAPS_SNAKE_CASE.
-2. Source Code Output
-- No inline comments, block comments, or JSDoc.
-- Do not generate explanatory comments in any code.
-- Code must stay deterministic and reproducible.
-- Write source code using snake_case conventions, while type names use PascalCase.
+3. Source Code
+- Keep code deterministic and reproducible.
+- Do not add source code comments.
+- Anchor comments are allowed only when they start with @ (e.g. // @todo).
+- /* */ blocks are allowed only to disable features during development and must not include descriptive text.
+- <!-- --> HTML blocks are allowed only to disable features during development or compiler/lint suppression (e.g. <!-- svelte-ignore ... -->).
-1. DRY and Modularity
-- Do not duplicate logic within or across projects.
-- Extract shared logic into the appropriate package under packages/.
-- Prefer small, focused modules.
-- Shared utilities must live in shared packages, not in apps.
+1. Modularity
+- Do not duplicate logic.
+- Put shared or generalizable code in packages/.
+- Apps should rely on packages/ for shared utilities.
+- Treat @radroots/*-bindings as generated from .rs crates; do not edit or format their .ts outputs. If issues arise, change upstream .rs or report the error instead.
-1. Functional and Architectural Cleanliness
+1. Architecture
- Prefer pure functions.
- Prefer composition over inheritance.
-- Avoid side effects unless explicitly needed.
+- Avoid side effects unless required.
- Avoid global mutable state.
-
+1. Change Policy
+- Apply breaking changes when needed.
+- Do not add legacy or shim fixes.
diff --git a/app/.env.example b/app/.env.example
@@ -1,5 +1,6 @@
VITE_PUBLIC_DEFAULT_RELAYS=
VITE_PUBLIC_RADROOTS_API=
+VITE_PUBLIC_RADROOTS_MEDIA=
VITE_PUBLIC_KEYVAL_NAME=
VITE_PUBLIC_NDK_CACHE=
VITE_PUBLIC_NDK_CLIENT=
diff --git a/app/src/app.html b/app/src/app.html
@@ -4,8 +4,6 @@
<head>
<meta charset="utf-8" />
- <title>Rad Roots</title>
-
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
diff --git a/app/src/lib/_env.ts b/app/src/lib/_env.ts
@@ -4,6 +4,9 @@ if (!DEFAULT_RELAYS || typeof DEFAULT_RELAYS !== 'string') throw new Error('Miss
const RADROOTS_API = import.meta.env.VITE_PUBLIC_RADROOTS_API;
if (!RADROOTS_API || typeof RADROOTS_API !== 'string') throw new Error('Missing env var: VITE_PUBLIC_RADROOTS_API');
+const RADROOTS_MEDIA = import.meta.env.VITE_PUBLIC_RADROOTS_MEDIA;
+if (!RADROOTS_MEDIA || typeof RADROOTS_MEDIA !== 'string') throw new Error('Missing env var: VITE_PUBLIC_RADROOTS_MEDIA');
+
const KEYVAL_NAME = import.meta.env.VITE_PUBLIC_KEYVAL_NAME;
if (!KEYVAL_NAME || typeof KEYVAL_NAME !== 'string') throw new Error('Missing env var: VITE_PUBLIC_KEYVAL_NAME');
@@ -37,5 +40,6 @@ export const _env = {
PLATFORM_DESCRIPTION,
PLATFORM_NAME,
RADROOTS_API,
+ RADROOTS_MEDIA,
RADROOTS_RELAY,
} as const;
diff --git a/app/src/lib/utils/app/index.ts b/app/src/lib/utils/app/index.ts
@@ -17,6 +17,8 @@ import type { CallbackPromise } from "@radroots/utils";
import { reset_sql_cipher } from "./cipher";
import type { NavigationRoute } from "./routes";
+const ls_val = get_store(ls);
+
export const datastore = new WebDatastore(
cfg_datastore_key_map,
cfg_datastore_key_param_map,
@@ -69,9 +71,8 @@ export const restart = async (opts?: {
export const reset = async (): Promise<void> => {
try {
- const $ls = get_store(ls);
const confirm = await notif.confirm({
- message: `${$ls(`notification.device.reset`)}. ${$ls(`common.this_action_is_irreversible`)}. ${$ls(`common.do_you_want_to_continue_q`)}`
+ message: `${ls_val(`notification.device.reset`)}. ${ls_val(`common.this_action_is_irreversible`)}. ${ls_val(`common.do_you_want_to_continue_q`)}`
});
if (!confirm) return;
await nostr_keys.reset();
@@ -79,7 +80,7 @@ export const reset = async (): Promise<void> => {
await reset_sql_cipher(db.get_store_key());
await db.reinit();
goto(`/`);
- app_notify.set(`${$ls(`notification.device.reset_complete`)}`);
+ app_notify.set(`${ls_val(`notification.device.reset_complete`)}`);
} catch (e) {
handle_err(e, `reset`);
}
diff --git a/app/src/lib/utils/backup/export.ts b/app/src/lib/utils/backup/export.ts
@@ -6,7 +6,7 @@ import { throw_err } from "@radroots/utils";
import { createStore, get as idb_get, keys as idb_keys, type UseStore } from "idb-keyval";
import { app_cfg } from "../app/config";
-const $ls = get_store(ls);
+const ls_val = get_store(ls);
const parse_idb_entry = async (key: IDBValidKey, store: UseStore): Promise<[string, string] | undefined> => {
if (typeof key !== "string") return undefined;
@@ -18,7 +18,7 @@ const parse_idb_entry = async (key: IDBValidKey, store: UseStore): Promise<[stri
const export_datastore_state = async (): Promise<ExportedAppState["datastore"]> => {
await datastore.init();
const ds_cfg = datastore.get_config();
- if (!ds_cfg) throw_err($ls(`error.app.export.missing_datastore_config`));
+ if (!ds_cfg) throw_err(ls_val(`error.app.export.missing_datastore_config`));
const ds_store = createStore(ds_cfg.database, ds_cfg.store);
const ds_keys = await idb_keys(ds_store);
const entries: Record<string, unknown> = {};
@@ -31,7 +31,7 @@ const export_datastore_state = async (): Promise<ExportedAppState["datastore"]>
const export_nostr_keystore_state = async (): Promise<ExportedAppState["nostr_keystore"]> => {
const nostr_cfg = nostr_keys.get_config();
- if (!nostr_cfg) throw_err($ls(`error.app.export.missing_nostr_keystore_config`));
+ if (!nostr_cfg) throw_err(ls_val(`error.app.export.missing_nostr_keystore_config`));
const keys: ExportedAppState["nostr_keystore"]["keys"] = [];
try {
@@ -84,10 +84,10 @@ export const export_app_state = async (): Promise<void> => {
database: tangle_db_state
};
const ts = payload.exported_at.replace(/[:.]/g, "-");
- const filename_prefix = $ls(`common.radroots_app_state_filename_prefix`);
+ const filename_prefix = ls_val(`common.radroots_app_state_filename_prefix`);
download_json(payload, `${filename_prefix}-${ts}.json`);
} catch (e) {
handle_err(e, `export_app_state`);
- await notif.alert(`${$ls(`error.backup.export_failure`)}`);
+ await notif.alert(`${ls_val(`error.backup.export_failure`)}`);
}
};
diff --git a/app/src/lib/utils/backup/import.ts b/app/src/lib/utils/backup/import.ts
@@ -9,7 +9,7 @@ import { createStore, set as idb_set } from "idb-keyval";
import { reset_sql_cipher } from "../app/cipher";
import { app_cfg } from "../app/config";
-const $ls = get_store(ls);
+const ls_val = get_store(ls);
export type AppImportStateResult = {
pass: boolean;
@@ -23,7 +23,7 @@ const assert_config_match = (
): void => {
if (current.database !== incoming.database || current.store !== incoming.store) {
throw_err(
- $ls(`error.configuration.import.storage_mismatch`, {
+ ls_val(`error.configuration.import.storage_mismatch`, {
label,
app_database: current.database,
app_store: current.store,
@@ -36,15 +36,15 @@ const assert_config_match = (
export const validate_import_file = async (file: File | null): Promise<ImportableAppState> => {
const parsed: any = await parse_file_json(file)
- if (!parsed) throw_err($ls(`error.configuration.import.invalid_file_contents`))
+ if (!parsed) throw_err(ls_val(`error.configuration.import.invalid_file_contents`))
return await validate_import_state(parsed);
};
export const validate_import_state = async (state: any): Promise<ImportableAppState> => {
- if (!state || typeof state !== "object") throw_err($ls(`error.configuration.import.empty_file`));
+ if (!state || typeof state !== "object") throw_err(ls_val(`error.configuration.import.empty_file`));
if (state.backup_version !== app_cfg.backup.version) {
throw_err(
- $ls(`error.configuration.import.unsupported_backup_version`, {
+ ls_val(`error.configuration.import.unsupported_backup_version`, {
backup_version: state.backup_version,
expected_version: app_cfg.backup.version,
})
@@ -52,18 +52,18 @@ export const validate_import_state = async (state: any): Promise<ImportableAppSt
}
const backup_format = state?.versions?.backup_format ?? state?.versions?.dump_format;
if (!state.versions || !state.versions.app || !state.versions.tangle_sql || !backup_format) {
- throw_err($ls(`error.configuration.import.missing_version_metadata`));
+ throw_err(ls_val(`error.configuration.import.missing_version_metadata`));
}
const database = state.database ?? state.tangle_db;
const backup = database?.backup ?? database?.dump;
if (!state.datastore || !state.nostr_keystore || !database) {
- throw_err($ls(`error.configuration.import.missing_required_sections`));
+ throw_err(ls_val(`error.configuration.import.missing_required_sections`));
}
if (!backup || backup.format_version !== backup_format) {
- throw_err($ls(`error.configuration.import.database_format_mismatch`));
+ throw_err(ls_val(`error.configuration.import.database_format_mismatch`));
}
if (backup.tangle_sql_version !== state.versions.tangle_sql) {
- throw_err($ls(`error.configuration.import.tangle_sql_version_mismatch`));
+ throw_err(ls_val(`error.configuration.import.tangle_sql_version_mismatch`));
}
return {
...state,
@@ -75,7 +75,7 @@ export const validate_import_state = async (state: any): Promise<ImportableAppSt
const restore_datastore_state = async (state: ImportableAppState["datastore"]): Promise<void> => {
const ds_cfg = datastore.get_config();
- assert_config_match(ds_cfg, state.config, $ls(`common.datastore`));
+ assert_config_match(ds_cfg, state.config, ls_val(`common.datastore`));
const reset_res = await datastore.reset();
if (reset_res && "err" in reset_res) throw_err(reset_res.err);
const store = createStore(ds_cfg.database, ds_cfg.store);
@@ -89,7 +89,7 @@ const restore_nostr_keystore_state = async (
state: ImportableAppState["nostr_keystore"]
): Promise<void> => {
const nostr_cfg = nostr_keys.get_config();
- assert_config_match(nostr_cfg, state.config, $ls(`common.nostr_keystore`));
+ assert_config_match(nostr_cfg, state.config, ls_val(`common.nostr_keystore`));
const reset_res = await nostr_keys.reset();
if (reset_res && "err" in reset_res) throw_err(reset_res.err);
for (const key of state.keys) {
@@ -102,7 +102,7 @@ const restore_tangle_db_state = async (state: ImportableAppState["database"]): P
const current_store_key = db.get_store_key();
if (current_store_key !== state.store_key) {
throw_err(
- $ls(`error.configuration.import.tangle_store_key_mismatch`, {
+ ls_val(`error.configuration.import.tangle_store_key_mismatch`, {
app_store_key: current_store_key,
backup_store_key: state.store_key,
})
@@ -115,13 +115,13 @@ const restore_tangle_db_state = async (state: ImportableAppState["database"]): P
export const import_app_state = async (payload: ImportableAppState): Promise<AppImportStateResult | IError<string>> => {
try {
- if (typeof window === "undefined") return err_msg($ls(`error.client.undefined_window`));
+ if (typeof window === "undefined") return err_msg(ls_val(`error.client.undefined_window`));
const import_state = await validate_import_state(payload);
- assert_config_match(datastore.get_config(), import_state.datastore.config, $ls(`common.datastore`));
- assert_config_match(nostr_keys.get_config(), import_state.nostr_keystore.config, $ls(`common.nostr_keystore`));
+ assert_config_match(datastore.get_config(), import_state.datastore.config, ls_val(`common.datastore`));
+ assert_config_match(nostr_keys.get_config(), import_state.nostr_keystore.config, ls_val(`common.nostr_keystore`));
const current_store_key = db.get_store_key();
if (current_store_key !== import_state.database.store_key) {
- const message = $ls(`error.configuration.import.tangle_store_key_mismatch`, {
+ const message = ls_val(`error.configuration.import.tangle_store_key_mismatch`, {
app_store_key: current_store_key,
backup_store_key: import_state.database.store_key,
});
@@ -135,7 +135,7 @@ export const import_app_state = async (payload: ImportableAppState): Promise<App
return { pass: true }
} catch (e) {
handle_err(e, `import_app_state`);
- return { pass: false, message: $ls(`error.configuration.import.failure`) }
+ return { pass: false, message: ls_val(`error.configuration.import.failure`) }
}
};
diff --git a/app/src/lib/utils/config.ts b/app/src/lib/utils/config.ts
@@ -1,4 +1,4 @@
-import { _envLib } from "@radroots/apps-lib";
+import { _env_lib } from "@radroots/apps-lib";
import type { AppConfigRole } from "@radroots/apps-lib-pwa/types/app";
import { root_symbol } from "@radroots/utils";
import type { NostrEventTagClient } from "@radroots/utils-nostr";
@@ -32,7 +32,7 @@ export const cfg_delay = {
};
export const cfg_nostr = {
- relay_url: _envLib.RADROOTS_RELAY,
+ relay_url: _env_lib.RADROOTS_RELAY,
relay_pubkey: radroots_nostr_pubkey,
relay_polling_count_max: 10,
};
diff --git a/app/src/routes/(app)/profile/+page.svelte b/app/src/routes/(app)/profile/+page.svelte
@@ -4,7 +4,7 @@
import { ndk_user, parse_file_path } from "@radroots/apps-lib";
import { Profile } from "@radroots/apps-lib-pwa";
import type { IViewProfileData } from "@radroots/apps-lib-pwa/types/views/profile";
- import { handle_err, throw_err, type FilePath } from "@radroots/utils";
+ import { handle_err, throw_err } from "@radroots/utils";
import { onMount } from "svelte";
let data: IViewProfileData | undefined = $state(undefined);
@@ -14,13 +14,19 @@
onMount(async () => {
try {
+ // await init();
+ await db.init();
data = await load_data();
} catch (e) {
handle_err(e, `on_mount`);
- await route(`/`);
+ // await route(`/`);
}
});
+ const init = async (): Promise<void> => {
+ await db.init();
+ };
+
const load_data = async (): Promise<IViewProfileData | undefined> => {
const nostr_profile = await db.nostr_profile_find_one({
on: {
@@ -28,7 +34,6 @@
},
});
if ("err" in nostr_profile) throw_err(nostr_profile);
-
//await nostr_sync.metadata({ metadata: tb_nostr_profile.result }); // no await
return { profile: nostr_profile.result };
};
@@ -47,7 +52,7 @@
public_key: $ndk_user?.pubkey,
},
});
- if ("err" in tb_nostrprofile) throw_err(tb_nostrprofile); //@todo
+ if ("err" in tb_nostrprofile) throw_err(tb_nostrprofile); // @todo
/*await nostr_sync.metadata({
metadata: tb_nostrprofile.result,
}); // no await */
@@ -61,7 +66,7 @@
return void (await route(`/`));
const nostr_key = await nostr_keys.read($ndk_user.pubkey);
if ("err" in nostr_key) throw_err(nostr_key);
- if (photo_path) {
+ /*if (photo_path) {
const confirm = await notif.confirm({
message: is_photo_existing
? `${$ls(
@@ -76,50 +81,106 @@
: `${$ls(`common.continue`)}`,
});
if (!confirm) return void (await route(`/`));
- }
+ }*/
+
loading_photo_upload = true;
- let upload_file_path: FilePath | undefined = undefined;
- parse_file_path(photo_path);
+ const parsed_photo_path = parse_file_path(photo_path);
+ if (!parsed_photo_path) throw_err("error.upload.no_photo_path");
+
+ const upload_path =
+ "blob_path" in parsed_photo_path
+ ? parsed_photo_path.blob_path
+ : parsed_photo_path.file_path;
+ const upload_name =
+ "blob_path" in parsed_photo_path
+ ? parsed_photo_path.blob_name
+ : parsed_photo_path.file_name;
+
const profile_photo_curr = await db.media_image_find_one({
on: {
- file_path: photo_path,
+ file_path: upload_name,
},
});
- if ("result" in profile_photo_curr)
- upload_file_path = parse_file_path(
- profile_photo_curr.result.file_path,
- );
- else upload_file_path = parse_file_path(photo_path);
- if (!upload_file_path) throw_err(`error.util.parse_file_path`);
- const file_data = await fs.read_bin(upload_file_path.file_path);
- if ("err" in file_data) throw_err(file_data);
- const res_fetch_media_image_upload =
- await radroots.media_image_upload({
- file_path: upload_file_path,
- file_data,
- secret_key: nostr_key.secret_key,
- });
- if ("err" in res_fetch_media_image_upload)
- throw_err(res_fetch_media_image_upload);
- const { res_base: upload_res_base, res_path: upload_res_path } =
- res_fetch_media_image_upload;
- const tb_media_image_create = await db.media_image_create({
- file_path: upload_file_path.file_path,
- res_base: upload_res_base,
- res_path: upload_res_path,
- mime_type: upload_file_path.mime_type,
+ if ("err" in profile_photo_curr) throw_err(profile_photo_curr);
+ if (
+ "result" in profile_photo_curr &&
+ profile_photo_curr.result
+ ) {
+ alert("todo, photo already added");
+ return;
+ //upload_file_path = parse_file_path(
+ // profile_photo_curr.result.file_path,
+ //);
+ }
+
+ const upload_bin = await fs.read_bin(upload_path);
+ if ("err" in upload_bin) throw_err(upload_bin);
+
+ const media_upload = await radroots.media_image_upload({
+ file_data: upload_bin,
+ secret_key: nostr_key.secret_key,
});
- if ("err" in tb_media_image_create)
- throw_err(tb_media_image_create);
+ console.log(
+ JSON.stringify(media_upload, null, 4),
+ `media_upload`,
+ );
+ if ("err" in media_upload) throw_err(media_upload);
+
+ const media_image = await db.media_image_create({
+ file_path: upload_path,
+ res_base: media_upload.base_url,
+ res_path: media_upload.hash,
+ mime_type: media_upload.ext,
+ });
+ console.log(
+ JSON.stringify(media_image, null, 4),
+ `media_image`,
+ );
+
+ if ("err" in media_image) throw_err(media_image);
+
const tb_nostr_profile_update = await db.nostr_profile_update({
on: { public_key: $ndk_user.pubkey },
fields: {
- picture: `${upload_res_base}/${upload_res_path}.${upload_file_path.mime_type}`,
+ picture: `${media_upload.base_url}/${media_upload.hash}.${media_upload.ext}`,
},
});
+ console.log(
+ JSON.stringify(tb_nostr_profile_update, null, 4),
+ `tb_nostr_profile_update`,
+ );
if ("err" in tb_nostr_profile_update)
throw_err(tb_nostr_profile_update);
await route(`/`);
+
+ //let upload_bin: WebFilePath | undefined = undefined;
+ //if ("blob_path" in upload_path)
+ //
+ // else upload_bin = await fs.read_bin(upload_path.file_path);
+
+ /*
+
+ console.log(`photo_path `, photo_path);
+
+ console.log(`photo_path_bin `, photo_path_bin);
+
+ let file_path_upload: FilePath | undefined = undefined;
+ console.log(`upload_file_path `, upload_file_path);
+
+
+
+ console.log(
+ JSON.stringify(profile_photo_curr, null, 4),
+ `profile_photo_curr`,
+ );
+
+
+ else upload_file_path = parse_file_path(photo_path);
+ if (!upload_file_path) throw_err(`error.util.parse_file_path`);
+ const file_data = await fs.read_bin(upload_file_path.file_path);
+ if ("err" in file_data) throw_err(file_data);
+
+ */
} catch (e) {
handle_err(e, `on_handle_back`);
} finally {
diff --git a/app/src/routes/(cfg)/setup/+page.svelte b/app/src/routes/(cfg)/setup/+page.svelte
@@ -75,7 +75,7 @@
let profile_name_val = $state(``);
let profile_name_valid = $state(false);
- let profile_name_nip05 = $state(false);
+ let profile_name_nip05 = $state(true);
let profile_name_loading = $state(false);
let is_eula_scrolled = $state(false);
diff --git a/app/src/routes/+layout.svelte b/app/src/routes/+layout.svelte
@@ -1,5 +1,6 @@
<script lang="ts">
import { dev, version as kit_version } from "$app/environment";
+ import { page } from "$app/state";
import { db, geoc } from "$lib/utils/app";
import { app_cfg } from "$lib/utils/app/config";
import {
@@ -24,6 +25,7 @@
import { app_lo } from "@radroots/apps-lib-pwa/stores/app";
import { cfg_app } from "@radroots/apps-lib-pwa/utils/app";
import { parse_theme_key, parse_theme_mode } from "@radroots/themes";
+ import { str_cap_words } from "@radroots/utils";
import "css-paint-polyfill";
import { onMount } from "svelte";
import "../app.css";
@@ -88,9 +90,16 @@
await db.init();
await geoc.connect();
});
+
+ const format_title = (title: string): string => {
+ return str_cap_words(title.replaceAll(`/`, ` `));
+ };
+
+ const head_title = $derived(format_title(page.url.pathname));
</script>
<svelte:head>
+ <title>{`${head_title || "Home"} | Rad Roots`}</title>
{#each head_meta_tags as meta_tag (meta_tag.name)}
<meta name={meta_tag.name} content={meta_tag.content} />
{/each}