5 Commits

Author SHA1 Message Date
Scott Idem
409308d2be Refine Jitsi docs and bootstrap notes
Keep the bootstrap quickstart focused on general platform knowledge, while preserving the Jitsi Reports reminder in the project docs and todo list.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-05 17:43:31 -04:00
Scott Idem
62cc26d1f9 Making things prettier:
npx prettier --write src/routes/journals/
2026-05-05 17:27:48 -04:00
Scott Idem
8b087edeb9 Add journal entry follow-up notes
Document the remaining Journal Entry Config follow-ups: toggle contrast, footer button styling, passcode auth behavior, AI summary shortcut, Archive On sizing, and Archive On behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-05 17:26:28 -04:00
Scott Idem
54707a00e3 Refine journal entry config
Polish the Journal Entry Config modal to match the desired section outline, hide alert messaging unless enabled, update the shared draft typing for entry flows, and replace deprecated privacy icons.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-05 17:14:20 -04:00
Scott Idem
e5c8500bc1 Advance journal config modal parity 2026-05-05 14:56:10 -04:00
26 changed files with 518 additions and 277 deletions

View File

@@ -191,7 +191,7 @@ This pattern lives in `ae_comp__badge_obj_li.svelte` — move to `ae_utils` if n
- Entry configuration admin controls are gated to `trusted_access` and above.
- `manager_access` and `administrator_access` see the Delete action, which performs a hard delete.
- `trusted_access` users see Remove instead, which follows disable semantics rather than a hard delete.
- The Admin section is the place for staff notes, enabled/default access state, and destructive entry actions; visibility/privacy flags remain separate.
- The Admin section is the place for staff notes, enabled/default access state, and destructive entry actions; the template toggle belongs in Metadata, while visibility/audience flags remain separate.
### Events — Badges

View File

@@ -326,9 +326,9 @@ These are real incidents — know them before you start.
See `GUIDE__AE_API_V3_for_Frontend.md` → section 3C for the full explanation.
12. **Broad Dexie result windows get silently clipped** — if a broad "All" view shows fewer
rows than a narrower filter, check for a page-level limit or an API revalidation step
replacing the local IDB result set. For empty text searches, the full local result set
should drive the display; server refreshes should update cache, not shrink visibility.
rows than a narrower filter, check for a page-level limit or an API revalidation step
replacing the local IDB result set. For empty text searches, the full local result set
should drive the display; server refreshes should update cache, not shrink visibility.
---

View File

@@ -461,6 +461,8 @@ Moderation permissions are controlled by `novi_jitsi_mod_li` in the IDAA store.
An admin/staff reporting tool that aggregates raw Jitsi activity logs into human-readable meeting sessions. It is **not** a member-facing page — IDAA members do not see it.
**Reminder:** this page is still incomplete. We still need the Novi UUID filter to work and we still need meeting-name whitelist filtering.
### View Modes
Two display modes, toggled via a button in the page header:
@@ -501,6 +503,8 @@ A "Reset Filters" button appears whenever any filter is non-default.
**Why display-name matching (not Novi UUID):** Jitsi participant data (`meta_json.participants`) only contains `displayName` and `role` — the Novi UUID is not passed through to the activity log. UUID-based exclusion would require a Jitsi config change plus a backend schema update and is deferred. Display names for OSIT staff are stable and controlled.
**Still pending:** the new Jitsi Reports page still needs a real Novi UUID filter and a whitelist of meeting names so staff can narrow the report set without relying only on display names.
### Summary Stats
Shown above the meeting list when data is loaded. Stats reflect the **filtered + exclusion-applied** view:

View File

@@ -151,9 +151,9 @@ Svelte 5 state is backed by Proxies.
### 5. Journal Entry Config Layout Notes
The Entry Config modal now follows a stricter section grammar:
* `Metadata` contains category, tags, summary, and archive date.
* `Metadata` contains category, tags, summary, archive date, and template.
* `Status & Security` contains enabled/hidden/priority/sort.
* `Privacy Flags` contains only visibility/audience toggles.
* `Visibility & Audience` contains only visibility/audience toggles.
* `Alerts & Messaging` contains alert flag + alert message.
* `Admin` is gated to trusted access and above, and is the only place for notes plus delete/remove actions.

View File

@@ -118,6 +118,22 @@ suddenly jumps to 0 errors, verify it's not because a bad `.d.ts` replaced a pac
Run `npx svelte-check 2>&1 | grep ModalProps` to get the current list.
Fix pattern: replace `children` prop binding with Svelte snippet syntax per flowbite-svelte docs.
### [Journals] Journal Entry Config follow-ups
- [ ] **[Journals] Visibility / audience toggle contrast** — the flag buttons need a clearer
selected state in both light and dark mode.
- [ ] **[Journals] Footer button style** — the actual `Done` button should read like a real button,
not a seamless footer spacer.
- [ ] **[Journals] Entry passcode secondary auth** — `passcode_hash` stores a hash; compare the
entered passcode hash to the stored hash, gate entry loading, and honor the TTL-based access
window. This is secondary entry auth, not a plain-text passcode field.
- [ ] **[Journals] Summary AI shortcut** — add an AI summarize button next to Entry Details
Summary so staff can generate a summary directly from the modal.
- [ ] **[Journals] Archive On sizing** — constrain the Archive On control to a reasonable width
instead of letting it expand to full width.
- [ ] **[Journals] Archive On behavior** — define what Archive On actually means and wire the
behavior; it is currently just a UI field with no live effect.
- [x] **[IDAA] Do not cache IDAA data in IDB when access is denied (2026-04-19, audited 2026-04-28)**
Full audit confirmed all protection layers are in place. No code changes required.
- All `+page.ts` / `+layout.ts` under `src/routes/idaa/` are clean — no SWR loads run before auth resolves.
@@ -168,6 +184,11 @@ suddenly jumps to 0 errors, verify it's not because a bad `.d.ts` replaced a pac
(`PATCH /v3/crud/site/{id}/`), reload `$ae_loc.site_cfg_json` on save so it takes
effect without re-login.
### [IDAA] Jitsi Reports still incomplete
- [ ] **Finish Jitsi Reports filters** — the new Jitsi Reports page still needs a working Novi UUID
filter and meeting-name whitelist filtering so staff can narrow the report set without relying on
display-name matching alone.
### [PWA] Service worker ignoring `chrome-extension://` requests
Browser console shows repeated errors:
```text

View File

@@ -24,8 +24,8 @@ import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.s
interface Props {
// Core Props
content: string; // The text to summarize/analyze
summary: string; // The result (bindable)
content: string | null | undefined; // The text to summarize/analyze
summary: string | null | undefined; // The result (bindable)
// Configuration (Bindable for global settings persistence)
model?: string;

View File

@@ -6,7 +6,7 @@
*/
import {
Siren,
Fingerprint,
FingerprintPattern,
Globe,
BookHeart,
BriefcaseBusiness,
@@ -14,11 +14,11 @@ import {
Settings
} from '@lucide/svelte';
import { ae_loc } from '$lib/stores/ae_stores';
import type { ae_JournalEntry } from '$lib/types/ae_types';
import type { ae_JournalEntryDraft } from '$lib/types/ae_types';
interface Props {
// The object containing the flags (bindable)
obj: ae_JournalEntry;
obj: ae_JournalEntryDraft;
// Visibility configuration (optional overrides)
show_labels?: boolean;
@@ -109,8 +109,8 @@ function toggle_template() {
onclick={toggle_private}
class="btn btn-sm flex items-center gap-2 px-3 transition preset-tonal-secondary hover:preset-filled-secondary-500"
title="Toggle private or encrypted visibility">
<Fingerprint size="1.2em" class={obj?.private ? 'text-success-500' : 'opacity-40'} />
<span class="whitespace-nowrap text-[10px] font-bold uppercase tracking-wider">Private</span>
<FingerprintPattern size="1.2em" class={obj?.private ? 'text-success-500' : 'opacity-40'} />
<span class="whitespace-nowrap text-[10px] font-bold uppercase tracking-wider">Private or Encrypt</span>
</button>
{/if}

View File

@@ -13,7 +13,7 @@ import { ensure_CodeMirror_modules } from './codemirror_modules';
// Icons (Standardized to Lucide where possible, or FontAwesome placeholders)
import { Bold, Code, Italic, List } from '@lucide/svelte';
interface Props {
content?: string;
content?: string | null;
new_content?: string;
placeholder?: string;
theme_mode?: 'light' | 'dark';

View File

@@ -230,6 +230,30 @@ export interface ae_JournalEntry extends ae_BaseObj {
file_count?: number;
}
export type ae_JournalEntryDraft = Omit<
Partial<ae_JournalEntry>,
| 'journal_entry_id'
| 'journal_id'
| 'name'
| 'summary'
| 'content'
| 'content_md_html'
| 'content_encrypted'
| 'history'
| 'history_encrypted'
> & {
journal_entry_id?: string;
journal_id?: string;
name?: string | null;
summary?: string | null;
content?: string | null;
content_md_html?: string | null;
content_encrypted?: string | null;
history?: string | null;
history_encrypted?: string | null;
[key: string]: unknown;
};
/**
* Person - A human entity
*/

View File

@@ -246,7 +246,7 @@ async function create_journal() {
{#if $journals_sess.show__modal__journals_config}
<AE_Comp_Modal_Journal_Config
show={$journals_sess.show__modal__journals_config} />
bind:show={$journals_sess.show__modal__journals_config} />
{/if}
<AE_Comp_Modal_Journal_Import

View File

@@ -83,11 +83,11 @@ $effect(() => {
class="
ae_journals__journal
mx-auto
flex w-full max-w-none min-w-0
flex min-h-0 w-full max-w-none
min-w-0
flex-col
items-stretch
gap-1
min-h-0
space-y-2
">
<div
@@ -197,12 +197,7 @@ Middle-click to open in new tab`}>
.length}&times; Recent Entries...
</option>
<!-- loop through each key value -->
{#each Object.entries(
$journals_loc.entry_view_history_kv as Record<
string,
{ id: string; name: string; url: string }
>
).reverse() as [journal_entry_id, journal_entry_obj] (journal_entry_id)}
{#each Object.entries($journals_loc.entry_view_history_kv as Record<string, { id: string; name: string; url: string }>).reverse() as [journal_entry_id, journal_entry_obj] (journal_entry_id)}
<option value={journal_entry_obj.id}>
{(journal_entry_obj?.name ||
journal_entry_obj?.id) ??
@@ -237,7 +232,9 @@ Middle-click to open in new tab`}>
</a>
{:else}
<!-- Edit Journal button. Creates a modal to edit the journal. -->
<Journal_entry_obj_qry {log_lvl} lq__journal_obj={$lq__journal_obj} />
<Journal_entry_obj_qry
{log_lvl}
lq__journal_obj={$lq__journal_obj} />
{/if}
<!-- Add default journal entry -->

View File

@@ -114,7 +114,10 @@ let lq__journal_entry_obj_li = $derived(
const journal_id = $lq__journal_obj?.journal_id;
return liveQuery(async () => {
if (params.remote_first && (!Array.isArray(ids) || ids.length === 0)) {
if (
params.remote_first &&
(!Array.isArray(ids) || ids.length === 0)
) {
return null;
}
@@ -258,14 +261,11 @@ async function handle_search_refresh(params: JournalSearchParams) {
const api_ids = api_results
.map((entry) => entry.id || entry.journal_entry_id)
.filter((entry): entry is string => Boolean(entry));
const display_ids = !qry_str && local_ids.length > 0 ? local_ids : api_ids;
const display_ids =
!qry_str && local_ids.length > 0 ? local_ids : api_ids;
// Protect UI cache if API returns empty during revalidation
if (
!qry_str &&
local_ids.length > 0 &&
api_ids.length === 0
) {
if (!qry_str && local_ids.length > 0 && api_ids.length === 0) {
untrack(() => {
$journals_sess.entry.qry__status = 'done';
});

View File

@@ -278,8 +278,8 @@ $effect(() => {
flex-col
items-stretch
gap-1
px-2 md:px-4
space-y-2
space-y-2 px-2
md:px-4
">
<!-- {#if $lq__journal_entry_obj} -->
<Journal_entry_view

View File

@@ -9,7 +9,7 @@ import { journals_loc } from '$lib/ae_journals/ae_journals_stores';
interface Props {
content: string;
summary: string; // Bindable
summary: string | null | undefined; // Bindable
on_save: () => void;
log_lvl?: number;
}
@@ -18,7 +18,8 @@ let { content, summary = $bindable(), on_save, log_lvl = 0 }: Props = $props();
</script>
<div class="journal-entry-floating-actions absolute top-14 right-2 z-10">
<div class="journal-entry-ai-tools inline-flex flex-wrap items-center justify-end gap-1">
<div
class="journal-entry-ai-tools inline-flex flex-wrap items-center justify-end gap-1">
<AE_AITools
{content}
model={$journals_loc.llm__api_model}

View File

@@ -8,17 +8,16 @@ import { LockKeyhole, RefreshCcw, Save } from '@lucide/svelte';
import { ae_loc } from '$lib/stores/ae_stores';
import { journals_loc } from '$lib/ae_journals/ae_journals_stores';
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
import type { ae_JournalEntry, ae_Journal } from '$lib/types/ae_types';
type JournalEntryDraft = Partial<ae_JournalEntry> & {
content?: string | false;
[key: string]: unknown;
};
import type {
ae_JournalEntry,
ae_Journal,
ae_JournalEntryDraft
} from '$lib/types/ae_types';
interface Props {
entry: ae_JournalEntry;
journal: ae_Journal;
tmp_entry_obj: JournalEntryDraft; // Bindable
tmp_entry_obj: ae_JournalEntryDraft; // Bindable
editor_view?: unknown; // Bindable
has_changed: boolean;
updated_idb: boolean;
@@ -40,7 +39,9 @@ let {
const is_editing = $derived(
$journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'
);
const preferred_viewer = $derived((journal?.cfg_json?.pref_viewer ?? 'rendered').toLowerCase());
const preferred_viewer = $derived(
(journal?.cfg_json?.pref_viewer ?? 'rendered').toLowerCase()
);
</script>
<div
@@ -59,7 +60,7 @@ const preferred_viewer = $derived((journal?.cfg_json?.pref_viewer ?? 'rendered')
class_li="w-full rounded-lg bg-surface-50 shadow-lg dark:bg-surface-800" />
{:else if preferred_viewer === 'plain'}
<div
class="bg-surface-50 dark:bg-surface-800 text-surface-800 dark:text-surface-100 w-full rounded-lg border border-gray-200 p-4 font-mono text-sm whitespace-pre-wrap wrap-break-word shadow-lg dark:border-gray-700">
class="bg-surface-50 dark:bg-surface-800 text-surface-800 dark:text-surface-100 w-full rounded-lg border border-gray-200 p-4 font-mono text-sm wrap-break-word whitespace-pre-wrap shadow-lg dark:border-gray-700">
{tmp_entry_obj?.content || ''}
</div>
{:else}
@@ -115,7 +116,7 @@ const preferred_viewer = $derived((journal?.cfg_json?.pref_viewer ?? 'rendered')
{:else}
<textarea
bind:value={tmp_entry_obj.content}
class="textarea h-125 w-full grow rounded-lg border-orange-500/30 p-4 font-mono whitespace-pre-wrap wrap-break-word shadow-lg"
class="textarea h-125 w-full grow rounded-lg border-orange-500/30 p-4 font-mono wrap-break-word whitespace-pre-wrap shadow-lg"
placeholder="Edit content..."></textarea>
{/if}

View File

@@ -100,11 +100,12 @@ function toggle_edit_mode() {
<input
type="text"
bind:value={tmp_entry_obj.name}
class="input input-sm h-10 min-w-0 flex-1 border-none bg-transparent text-lg font-bold leading-none transition-colors duration-150 focus:ring-2 focus:ring-primary-500"
class="input input-sm focus:ring-primary-500 h-10 min-w-0 flex-1 border-none bg-transparent text-lg leading-none font-bold transition-colors duration-150 focus:ring-2"
placeholder="Entry Title..."
onchange={on_save} />
{:else}
<h2 class="flex h-10 min-w-0 flex-1 items-center truncate text-base font-bold leading-none md:text-lg">
<h2
class="flex h-10 min-w-0 flex-1 items-center truncate text-base leading-none font-bold md:text-lg">
{entry.name ||
ae_util.iso_datetime_formatter(
entry.created_on,
@@ -115,14 +116,16 @@ function toggle_edit_mode() {
</div>
</div>
<div class="ml-auto flex shrink-0 items-center gap-2 whitespace-nowrap transition-colors duration-150">
<div
class="ml-auto flex shrink-0 items-center gap-2 whitespace-nowrap transition-colors duration-150">
<!-- Auto-Save indicator -->
{#if $journals_loc.entry.edit_kv[entry.journal_entry_id] === 'current'}
<div
class="border-surface-500/20 mr-1 flex items-center gap-1 border-r px-2">
<button
type="button"
class="btn-icon btn-icon-sm transition-colors duration-150 {$journals_loc.entry.auto_save
class="btn-icon btn-icon-sm transition-colors duration-150 {$journals_loc
.entry.auto_save
? 'text-primary-500'
: 'opacity-30'}"
onclick={() =>

View File

@@ -231,7 +231,8 @@ async function handle_remove_file(file_id: string) {
file.hosted_file_id || file.id || file.hosted_file_id}
<div
class="bg-surface-50-950 border-surface-500/10 group hover:border-primary-500 flex items-center justify-between rounded-xl border p-3 shadow-sm transition-all">
<div class="flex grow items-center gap-3 overflow-hidden">
<div
class="flex grow items-center gap-3 overflow-hidden">
<AE_Comp_Hosted_Files_Download_Button
hosted_file_id={file_id}
hosted_file_obj={file}
@@ -281,7 +282,9 @@ async function handle_remove_file(file_id: string) {
<FileUp size="1.8em" />
</div>
<div class="text-center">
<p class="font-bold">Upload new attachment</p>
<p class="font-bold">
Upload new attachment
</p>
<p class="text-xs opacity-60">
Drag and drop or click to browse
</p>

View File

@@ -31,6 +31,7 @@ import AE_Comp_Journal_Entry_AiTools from './ae_comp__journal_entry_ai_tools.sve
import AE_Comp_Journal_Entry_ObjFileLi from './ae_comp__journal_entry_obj_file_li.svelte';
import AE_Comp_Modal_Journal_Entry_Append from './ae_comp__modal_journal_entry_append.svelte';
import AE_Comp_Modal_Journal_Entry_Config from './ae_comp__modal_journal_entry_config.svelte';
import type { ae_JournalEntryDraft } from '$lib/types/ae_types';
// Icons
import { CircleAlert, CircleX, LoaderCircle } from '@lucide/svelte';
@@ -53,8 +54,8 @@ let {
// *** State
let editor_view: any = $state();
let orig_entry_obj: key_val | null = $state(null);
let tmp_entry_obj: key_val = $state({});
let orig_entry_obj: ae_JournalEntryDraft | null = $state(null);
let tmp_entry_obj: ae_JournalEntryDraft = $state({});
let save_status: 'saved' | 'unsaved' | 'saving' = $state('saved');
let decryption_error: string | null = $state(null);
let auto_save_timer: ReturnType<typeof setTimeout>;
@@ -65,7 +66,7 @@ let show_config_modal = $state(false);
function deep_copy(obj: any) {
if (!obj) return null;
try {
const copy: key_val = {};
const copy: ae_JournalEntryDraft = {};
for (const key in obj) {
const val = obj[key];
if (val instanceof Date) {
@@ -240,9 +241,12 @@ async function run_decryption_workflow() {
}
// SUCCESS
const content = result.content || '';
const content =
typeof result.content === 'string'
? result.content
: await (result.content ?? '');
tmp_entry_obj.content = content;
tmp_entry_obj.content_md_html = handle_marked(content);
tmp_entry_obj.content_md_html = await handle_marked(content);
tmp_entry_obj.content_encrypted = null;
tmp_entry_obj.history_encrypted = null;
@@ -303,8 +307,12 @@ async function update_journal_entry(fields_kv?: key_val) {
if (!fields_kv) {
if (tmp_entry_obj.private) {
if (tmp_entry_obj.content) {
const content_to_encrypt =
typeof tmp_entry_obj.content === 'string'
? tmp_entry_obj.content
: '';
data_kv.content_encrypted = await ae_util.encrypt_wrapper(
tmp_entry_obj.content,
content_to_encrypt,
decrypt_key
);
data_kv.content = null;
@@ -316,9 +324,15 @@ async function update_journal_entry(fields_kv?: key_val) {
}
try {
const journal_entry_id = $lq__journal_entry_obj?.journal_entry_id;
if (!journal_entry_id) {
console.error('Journal entry ID missing for update.');
return;
}
await journals_func.update_ae_obj__journal_entry({
api_cfg: $ae_api,
journal_entry_id: $lq__journal_entry_obj?.journal_entry_id,
journal_entry_id,
data_kv: data_kv,
log_lvl: 1
});
@@ -424,9 +438,9 @@ let modal_mode: 'append' | 'prepend' | 'auto' = $state('auto');
group-hover/entry-view:opacity-15 dark:opacity-10 dark:group-hover/entry-view:opacity-20">
</div>
<section
class="ae_view relative flex w-full min-w-0 flex-col gap-4 rounded-xl
border border-surface-200-800
bg-surface-50-900 p-2 shadow-xl"
class="ae_view border-surface-200-800 bg-surface-50-900 relative flex w-full min-w-0 flex-col
gap-4 rounded-xl
border p-2 shadow-xl"
bind:clientHeight={$ae_loc.iframe_height_modal_body}>
{#if $lq__journal_entry_obj && $lq__journal_obj}
<AE_Comp_Journal_Entry_Header
@@ -445,7 +459,8 @@ let modal_mode: 'append' | 'prepend' | 'auto' = $state('auto');
{#if decryption_error}
<div
class="bg-error-100 border-error-300 flex w-full animate-bounce items-center justify-between rounded-lg border p-4 shadow-xl">
<div class="text-error-700 flex items-center gap-4 font-bold">
<div
class="text-error-700 flex items-center gap-4 font-bold">
<CircleAlert size="2.5em" />
<span class="text-xl">{decryption_error}</span>
</div>
@@ -460,15 +475,17 @@ let modal_mode: 'append' | 'prepend' | 'auto' = $state('auto');
<!-- ring-2 inset indicates "edit mode" in both light and dark without a background swap -->
<section
class="relative grow overflow-hidden rounded-xl border border-surface-200-800
bg-surface-100-900 p-2 shadow-md min-w-0
class="border-surface-200-800 bg-surface-100-900 relative min-w-0 grow overflow-hidden
rounded-xl border p-2 shadow-md
{$journals_loc.entry.edit_kv[
$lq__journal_entry_obj?.journal_entry_id
] == 'current'
? 'ring-primary-500/40 ring-2 ring-inset'
: ''}">
<AE_Comp_Journal_Entry_AiTools
content={tmp_entry_obj.content}
content={typeof tmp_entry_obj.content === 'string'
? tmp_entry_obj.content
: ''}
bind:summary={tmp_entry_obj.summary}
on_save={() => update_journal_entry()}
{log_lvl} />
@@ -491,8 +508,9 @@ let modal_mode: 'append' | 'prepend' | 'auto' = $state('auto');
{lq__journal_entry_obj} />
<div
class="rounded-xl border border-surface-200-800 bg-surface-100-900 p-3 shadow-sm">
<AE_Comp_Journal_Entry_Metadata entry={$lq__journal_entry_obj} />
class="border-surface-200-800 bg-surface-100-900 rounded-xl border p-3 shadow-sm">
<AE_Comp_Journal_Entry_Metadata
entry={$lq__journal_entry_obj} />
</div>
</div>
@@ -509,7 +527,6 @@ let modal_mode: 'append' | 'prepend' | 'auto' = $state('auto');
<AE_Comp_Modal_Journal_Entry_Config
bind:show={show_config_modal}
entry={$lq__journal_entry_obj}
journal={$lq__journal_obj}
bind:tmp_entry_obj
on_save={() => update_journal_entry()}

View File

@@ -52,8 +52,9 @@ import {
} from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import AeCompModalJournalEntryAppend from './ae_comp__modal_journal_entry_append.svelte';
import type { ae_JournalEntryDraft } from '$lib/types/ae_types';
let tmp_entry_obj: key_val = $state({});
let tmp_entry_obj: ae_JournalEntryDraft = $state({});
// Derived state for modal visibility
// We cast to boolean for the prop, but we need to handle the close event to clear the store ID
@@ -106,9 +107,7 @@ let visible_journal_entry_obj_li = $derived(
</div>
<section
class="journal_list relative flex w-full flex-col items-center justify-center gap-1 md:gap-2">
{#if visible_journal_entry_obj_li === null ||
($journals_sess.entry.qry__status === 'loading' &&
visible_journal_entry_obj_li.length === 0)}
{#if visible_journal_entry_obj_li === null || ($journals_sess.entry.qry__status === 'loading' && visible_journal_entry_obj_li.length === 0)}
<!-- Loading state -->
<div
class="flex flex-col items-center justify-center p-10 opacity-50">

View File

@@ -16,14 +16,9 @@ interface Props {
let { log_lvl = $bindable(0), lq__journal_obj }: Props = $props();
import {
Library,
RemoveFormatting
} from '@lucide/svelte';
import { Library, RemoveFormatting } from '@lucide/svelte';
import {
ae_loc,
} from '$lib/stores/ae_stores';
import { ae_loc } from '$lib/stores/ae_stores';
import {
journals_loc,
journals_sess
@@ -127,10 +122,13 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
{#if $ae_loc.edit_mode && ($ae_loc.manager_access || $ae_loc.trusted_access)}
<span class="flex flex-row flex-wrap items-center gap-2">
<span class="hidden text-sm text-gray-500 lg:inline"> Filters: </span>
<span class="hidden text-sm text-gray-500 lg:inline">
Filters:
</span>
{#if $ae_loc.manager_access}
<label class="flex flex-row items-center gap-1 text-xs font-semibold text-gray-500">
<label
class="flex flex-row items-center gap-1 text-xs font-semibold text-gray-500">
<span>Enabled</span>
<select
class="select select-sm"
@@ -145,7 +143,8 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
{/if}
{#if $ae_loc.trusted_access}
<label class="flex flex-row items-center gap-1 text-xs font-semibold text-gray-500">
<label
class="flex flex-row items-center gap-1 text-xs font-semibold text-gray-500">
<span>Hidden</span>
<select
class="select select-sm"

View File

@@ -312,7 +312,7 @@ async function delete_journal() {
{/each}
</select>
</label>
<label class="label flex flex-col items-start gap-1">
<label class="label flex flex-col items-start gap-1">
<span class="text-sm font-bold opacity-70"
>Journal Group (text)</span>
<input

View File

@@ -236,7 +236,6 @@ async function handle_new_entry() {
{/if}
</div>
</header>
</section>
</div>

View File

@@ -8,12 +8,10 @@ import { untrack } from 'svelte';
import {
CalendarClock,
Check,
CircleAlert,
CodeXml,
Database,
Layout,
MonitorPlay,
MousePointerClick,
Palette,
Settings,
ShieldCheck,
Wrench,
@@ -22,7 +20,8 @@ import {
import { Modal } from 'flowbite-svelte';
// *** Import Aether specific variables and functions
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { ae_loc } from '$lib/stores/ae_stores';
import type { key_val } from '$lib/stores/ae_stores';
import {
journals_loc,
journals_sess
@@ -30,24 +29,29 @@ import {
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
interface Props {
log_lvl?: number;
show?: boolean;
}
let { log_lvl = 0, show = $bindable(false) }: Props = $props();
let { show = $bindable(false) }: Props = $props();
// Internal State
let tab: 'form' | 'local_json' | 'session_json' = $state('form');
let tmp_config: any = $state({
let tab: 'config' | 'local_json' | 'session_json' = $state('config');
let tmp_config: key_val = $state({
journal: {},
entry: {}
});
const panel_class = 'space-y-4 rounded-xl border border-surface-500/20 bg-surface-500/5 p-4 shadow-sm';
const panel_title_class = 'flex items-center gap-2 border-b border-surface-500/20 pb-2 text-lg font-bold';
const panel_class =
'space-y-4 rounded-xl border border-surface-500/20 bg-surface-500/5 p-4 shadow-sm';
const panel_title_class =
'flex items-center gap-2 border-b border-surface-500/20 pb-2 text-lg font-bold';
const field_card_class =
'bg-surface-500/5 border-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-3 transition-colors hover:bg-surface-500/10';
const tab_button_base_class = 'btn btn-sm border transition-all duration-200';
const tab_button_active_class = 'border-surface-200-800 bg-surface-200-800 text-surface-950-50 shadow-sm';
const tab_button_inactive_class = 'border-transparent bg-surface-50-900/60 text-surface-600-400 hover:border-surface-200-800 hover:bg-surface-100-900 hover:text-surface-950-50';
const tab_button_active_class =
'border-surface-200-800 bg-surface-200-800 text-surface-950-50 shadow-sm';
const tab_button_inactive_class =
'border-transparent bg-surface-50-900/60 text-surface-600-400 hover:border-surface-200-800 hover:bg-surface-100-900 hover:text-surface-950-50';
function tab_button_class(is_active: boolean): string {
return `${tab_button_base_class} ${is_active ? tab_button_active_class : tab_button_inactive_class}`;
@@ -76,7 +80,7 @@ function handle_save() {
dismissable={false}
placement="top-center"
size="xl"
class="relative mx-auto flex h-[calc(100dvh-2rem)] max-h-[calc(100dvh-2rem)] w-full flex-col rounded-xl border border-surface-200-800 bg-surface-50-900 text-surface-950-50 shadow-xl"
class="border-surface-200-800 bg-surface-50-900 text-surface-950-50 relative mx-auto flex h-[calc(100dvh-2rem)] max-h-[calc(100dvh-2rem)] w-full flex-col rounded-xl border shadow-xl"
headerClass="flex w-full flex-row items-center justify-between gap-2 rounded-t-xl border-b border-surface-200-800 bg-surface-100-900 p-4"
footerClass="flex w-full flex-row items-center justify-center gap-2 rounded-b-xl border-t border-surface-200-800 bg-surface-100-900 p-4">
{#snippet header()}
@@ -98,8 +102,8 @@ function handle_save() {
class="bg-surface-500/10 sticky top-0 z-10 mx-auto mb-4 flex max-w-fit justify-center gap-1 rounded-lg p-1 backdrop-blur-sm">
<button
type="button"
class={tab_button_class(tab === 'form')}
onclick={() => (tab = 'form')}>
class={tab_button_class(tab === 'config')}
onclick={() => (tab = 'config')}>
<Settings size="1.1em" class="mr-1" /> Config
</button>
<button
@@ -116,16 +120,16 @@ function handle_save() {
</button>
</div>
{#if tab === 'form'}
<div class="animate-in fade-in space-y-8 duration-300">
{#if tab === 'config'}
<div class="animate-in fade-in space-y-4 duration-300">
<!-- Date/Time Section -->
<section class={panel_class}>
<h2 class={panel_title_class}>
<CalendarClock size="1.2em" class="text-primary-500" />
Date and Time Display
</h2>
<div class="grid grid-cols-1 gap-6 p-2 md:grid-cols-2">
<label class="label">
<div class="grid grid-cols-1 gap-4 p-2 md:grid-cols-2">
<label class="label flex flex-col items-start gap-1">
<span class="text-sm font-bold opacity-70"
>DateTime Format</span>
<select
@@ -145,7 +149,7 @@ function handle_save() {
>ISO (YYYY-MM-DD HH:mm:ss)</option>
</select>
</label>
<label class="label">
<label class="label flex flex-col items-start gap-1">
<span class="text-sm font-bold opacity-70"
>Time-Only Format</span>
<select
@@ -172,9 +176,8 @@ function handle_save() {
class="text-primary-500" />
User Interface Preferences
</h2>
<div class="grid grid-cols-1 gap-6 p-2 md:grid-cols-2">
<label
class="bg-surface-500/5 border-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-2">
<div class="grid grid-cols-1 gap-4 p-2 md:grid-cols-2">
<label class={field_card_class}>
<input
type="checkbox"
bind:checked={tmp_config.entry.auto_save}
@@ -186,8 +189,7 @@ function handle_save() {
</p>
</div>
</label>
<label
class="bg-surface-500/5 border-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-2">
<label class={field_card_class}>
<input
type="checkbox"
bind:checked={tmp_config.show_id_random}
@@ -209,8 +211,8 @@ function handle_save() {
<Database size="1.2em" class="text-primary-500" />
Journal Query Filters
</h2>
<div class="grid grid-cols-1 gap-6 p-2 md:grid-cols-3">
<label class="label">
<div class="grid grid-cols-1 gap-4 p-2 md:grid-cols-3">
<label class="label flex flex-col items-start gap-1">
<span class="text-sm font-bold opacity-70"
>Enabled Status</span>
<select
@@ -223,7 +225,7 @@ function handle_save() {
>All (Enabled & Disabled & NULL)</option>
</select>
</label>
<label class="label">
<label class="label flex flex-col items-start gap-1">
<span class="text-sm font-bold opacity-70"
>Hidden Status</span>
<select
@@ -235,7 +237,7 @@ function handle_save() {
>All (Visible & Hidden & NULL)</option>
</select>
</label>
<label class="label">
<label class="label flex flex-col items-start gap-1">
<span class="text-sm font-bold opacity-70"
>Query Limit</span>
<select
@@ -259,8 +261,8 @@ function handle_save() {
<Database size="1.2em" class="text-primary-500" />
Entry Query Filters
</h2>
<div class="grid grid-cols-1 gap-6 p-2 md:grid-cols-3">
<label class="label">
<div class="grid grid-cols-1 gap-4 p-2 md:grid-cols-3">
<label class="label flex flex-col items-start gap-1">
<span class="text-sm font-bold opacity-70"
>Enabled Status</span>
<select
@@ -273,7 +275,7 @@ function handle_save() {
>All (Enabled & Disabled & NULL)</option>
</select>
</label>
<label class="label">
<label class="label flex flex-col items-start gap-1">
<span class="text-sm font-bold opacity-70"
>Hidden Status</span>
<select
@@ -285,7 +287,7 @@ function handle_save() {
>All (Visible & Hidden & NULL)</option>
</select>
</label>
<label class="label">
<label class="label flex flex-col items-start gap-1">
<span class="text-sm font-bold opacity-70"
>Query Limit</span>
<select
@@ -309,12 +311,12 @@ function handle_save() {
<ShieldCheck size="1.2em" class="text-primary-500" />
Security & Encryption
</h2>
<div class="space-y-4 rounded-lg border border-surface-500/10 bg-surface-500/5 p-4">
<div
class="border-surface-500/10 bg-surface-500/5 space-y-4 rounded-lg border p-4">
<div class="text-sm italic opacity-80">
Global security overrides for the journal module.
</div>
<label
class="flex cursor-pointer items-center space-x-3">
<label class={field_card_class}>
<input
type="checkbox"
bind:checked={
@@ -326,6 +328,125 @@ function handle_save() {
</label>
</div>
</section>
<!-- Alerts Section -->
<section class={panel_class}>
<h2 class={panel_title_class}>
<CircleAlert size="1.2em" class="text-primary-500" />
Alerts & Messaging
</h2>
<div
class="border-surface-500/10 bg-surface-500/5 space-y-4 rounded-xl border p-4">
<label class="label flex flex-col items-start gap-1">
<span class="text-sm font-bold opacity-70"
>Alert Message</span>
<textarea
bind:value={tmp_config.alert_msg}
class="textarea min-h-24 w-full"
placeholder="Optional alert or notice shown with the journal"
></textarea>
</label>
<label class={field_card_class}>
<input
type="checkbox"
bind:checked={tmp_config.alert}
class="checkbox checkbox-primary" />
<div class="flex flex-col">
<span class="font-bold">Enable Alert</span>
<span class="text-xs opacity-60">
Mark this journal for attention in lists and
headers.
</span>
</div>
</label>
</div>
</section>
<!-- Admin Section -->
<section class={panel_class}>
<h2 class={panel_title_class}>
<Settings size="1.2em" class="text-primary-500" />
Admin
</h2>
<div
class="border-surface-500/10 bg-surface-500/5 space-y-4 rounded-xl border p-4">
<label class="label flex flex-col items-start gap-1">
<span class="text-sm font-bold opacity-70"
>Notes</span>
<textarea
bind:value={tmp_config.notes}
class="textarea min-h-24 w-full"
placeholder="Internal notes for administrators and trusted access"
></textarea>
</label>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<label class={field_card_class}>
<input
type="checkbox"
bind:checked={tmp_config.enable}
class="checkbox checkbox-primary" />
<div class="flex flex-col">
<span class="font-bold">Enabled</span>
<span class="text-xs opacity-60">
Allow access to this journal.
</span>
</div>
</label>
<label class={field_card_class}>
<input
type="checkbox"
bind:checked={tmp_config.hide}
class="checkbox checkbox-primary" />
<div class="flex flex-col">
<span class="font-bold">Hidden</span>
<span class="text-xs opacity-60">
Hide this journal from standard lists.
</span>
</div>
</label>
<label class={field_card_class}>
<input
type="checkbox"
bind:checked={tmp_config.priority}
class="checkbox checkbox-primary" />
<div class="flex flex-col">
<span class="font-bold">Priority</span>
<span class="text-xs opacity-60">
Pin this journal ahead of the standard
order.
</span>
</div>
</label>
<label
class="bg-surface-500/5 border-surface-500/10 flex items-center justify-between rounded-lg border p-3">
<div class="flex flex-col">
<span class="text-sm font-bold"
>Sort Order</span>
<span class="text-xs opacity-60">
Lower numbers appear earlier in lists.
</span>
</div>
<input
type="number"
bind:value={tmp_config.sort}
class="input input-sm w-24 text-right" />
</label>
</div>
<div
class="border-surface-500/10 bg-surface-500/5 rounded-xl border p-4">
<p class="text-xs italic opacity-70">
Admin changes apply to the journal module state
and are saved with the rest of this config.
</p>
</div>
</div>
</section>
</div>
{:else if tab === 'local_json'}
<div class="h-full min-h-100">

View File

@@ -5,10 +5,11 @@ import { ae_util } from '$lib/ae_utils/ae_utils';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import { ae_api } from '$lib/stores/ae_stores';
import type { key_val } from '$lib/stores/ae_stores';
import type { ae_JournalEntryDraft } from '$lib/types/ae_types';
interface Props {
open: boolean;
journal_entry: key_val;
journal_entry: ae_JournalEntryDraft;
journal_config: key_val; // The cfg_json from the journal object
mode?: 'append' | 'prepend' | 'auto';
on_close: () => void;
@@ -26,7 +27,7 @@ let {
log_lvl = 0
}: Props = $props();
// Local State
let tmp_entry_obj: key_val = $state({});
let tmp_entry_obj: ae_JournalEntryDraft = $state({});
// Header Options
let add_timestamp_header: boolean = $state(true);
@@ -50,7 +51,8 @@ $effect(() => {
});
async function handle_save() {
let current_entry_content = tmp_entry_obj?.content || '';
let current_entry_content =
typeof tmp_entry_obj?.content === 'string' ? tmp_entry_obj.content : '';
let add_content = '';
let new_content = current_entry_content;
@@ -109,11 +111,16 @@ async function handle_save() {
new_content = new_content.trim() + '\n';
let data_kv = { content: new_content };
const journal_entry_id = tmp_entry_obj.journal_entry_id;
if (!journal_entry_id) {
console.error('Journal entry ID missing for append save.');
return;
}
try {
let update_result = await journals_func.update_ae_obj__journal_entry({
api_cfg: $ae_api,
journal_entry_id: tmp_entry_obj?.journal_entry_id,
journal_entry_id,
data_kv: data_kv,
log_lvl: log_lvl
});

View File

@@ -7,13 +7,11 @@ import {
ArrowDownToLine,
ArrowUpToLine,
Check,
CircleAlert,
CodeXml,
Copy,
FileDown,
Fingerprint,
FingerprintPattern,
Minus,
Plus,
RefreshCcw,
Settings,
Shapes,
@@ -29,13 +27,13 @@ import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
import AE_Object_Flags from '$lib/ae_elements/AE_Object_Flags.svelte';
import type { ae_Journal, ae_JournalEntry } from '$lib/types/ae_types';
import type { ae_Journal, ae_JournalEntryDraft } from '$lib/types/ae_types';
interface Props {
log_lvl?: number;
show?: boolean;
journal: ae_Journal;
tmp_entry_obj: ae_JournalEntry; // Bindable
tmp_entry_obj: ae_JournalEntryDraft; // Bindable
on_save: () => void;
on_force_reset?: () => void;
on_show_export?: () => void;
@@ -63,12 +61,17 @@ const normalize_date = (val?: string | Date | null) => {
return val.slice(0, 16);
};
const panel_class = 'space-y-4 rounded-xl border border-surface-500/20 bg-surface-500/5 p-4 shadow-sm';
const panel_title_class = 'flex items-center gap-2 border-b border-surface-500/20 pb-2 text-lg font-bold';
const field_card_class = 'bg-surface-500/5 border-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-3 transition-colors hover:bg-surface-500/10';
const panel_class =
'space-y-4 rounded-xl border border-surface-500/20 bg-surface-500/5 p-4 shadow-sm';
const panel_title_class =
'flex items-center gap-2 border-b border-surface-500/20 pb-2 text-lg font-bold';
const field_card_class =
'bg-surface-500/5 border-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-3 transition-colors hover:bg-surface-500/10';
const tab_button_base_class = 'btn btn-sm border transition-all duration-200';
const tab_button_active_class = 'border-surface-200-800 bg-surface-200-800 text-surface-950-50 shadow-sm';
const tab_button_inactive_class = 'border-transparent bg-surface-50-900/60 text-surface-600-400 hover:border-surface-200-800 hover:bg-surface-100-900 hover:text-surface-950-50';
const tab_button_active_class =
'border-surface-200-800 bg-surface-200-800 text-surface-950-50 shadow-sm';
const tab_button_inactive_class =
'border-transparent bg-surface-50-900/60 text-surface-600-400 hover:border-surface-200-800 hover:bg-surface-100-900 hover:text-surface-950-50';
function tab_button_class(is_active: boolean): string {
return `${tab_button_base_class} ${is_active ? tab_button_active_class : tab_button_inactive_class}`;
@@ -77,6 +80,12 @@ function tab_button_class(is_active: boolean): string {
async function handle_update_entry() {
try {
// WHITELISTED BASE TABLE COLUMNS ONLY
const journal_entry_id = tmp_entry_obj.journal_entry_id;
if (!journal_entry_id) {
console.error('Journal entry ID missing for update.');
return;
}
const data_kv = {
name: tmp_entry_obj.name,
short_name: tmp_entry_obj.short_name,
@@ -86,6 +95,7 @@ async function handle_update_entry() {
type_code: tmp_entry_obj.type_code,
topic_code: tmp_entry_obj.topic_code,
tags: tmp_entry_obj.tags,
passcode_hash: tmp_entry_obj.passcode_hash,
private: tmp_entry_obj.private,
public: tmp_entry_obj.public,
personal: tmp_entry_obj.personal,
@@ -105,7 +115,7 @@ async function handle_update_entry() {
await journals_func.update_ae_obj__journal_entry({
api_cfg: $ae_api,
journal_entry_id: tmp_entry_obj.journal_entry_id,
journal_entry_id,
data_kv: data_kv,
log_lvl: log_lvl
});
@@ -119,15 +129,25 @@ async function handle_admin_delete_action() {
const delete_method = can_delete ? 'delete' : 'disable';
const action_label = can_delete ? 'delete' : 'remove';
const confirm_label = can_delete ? 'delete' : 'remove';
const journal_entry_id = tmp_entry_obj.journal_entry_id;
if (!confirm(`Are you sure you want to ${confirm_label} this journal entry?`)) {
if (!journal_entry_id) {
console.error('Journal entry ID missing for delete action.');
return;
}
if (
!confirm(
`Are you sure you want to ${confirm_label} this journal entry?`
)
) {
return;
}
try {
await journals_func.delete_ae_obj_id__journal_entry({
api_cfg: $ae_api,
journal_entry_id: tmp_entry_obj.journal_entry_id,
journal_entry_id,
method: delete_method,
log_lvl
});
@@ -135,7 +155,10 @@ async function handle_admin_delete_action() {
show = false;
await goto(`/journals/${tmp_entry_obj.journal_id}`);
} catch (error) {
console.error(`Error attempting to ${action_label} journal entry:`, error);
console.error(
`Error attempting to ${action_label} journal entry:`,
error
);
alert(`Failed to ${action_label} journal entry.`);
}
}
@@ -147,9 +170,9 @@ async function handle_admin_delete_action() {
dismissable={false}
placement="top-center"
size="lg"
class="relative mx-auto flex h-[calc(100dvh-2rem)] max-h-[calc(100dvh-2rem)] w-full flex-col rounded-xl border border-surface-200-800 bg-surface-50-900 text-surface-950-50 shadow-xl"
class="border-surface-200-800 bg-surface-50-900 text-surface-950-50 relative mx-auto flex h-[calc(100dvh-2rem)] max-h-[calc(100dvh-2rem)] w-full flex-col rounded-xl border shadow-xl"
headerClass="flex w-full flex-row items-center justify-between gap-2 rounded-t-xl border-b border-surface-200-800 bg-surface-100-900 p-4"
footerClass="flex w-full flex-row items-center justify-center gap-2 rounded-b-xl border-t border-surface-200-800 bg-surface-100-900 p-4">
footerClass="mt-auto flex w-full shrink-0 flex-row items-center justify-center gap-2 rounded-b-xl border-t border-surface-200-800 bg-surface-100-900 p-4">
{#snippet header()}
<h3 class="flex flex-1 items-center gap-2 text-lg font-bold">
<Settings class="text-primary-500" />
@@ -338,6 +361,25 @@ async function handle_admin_delete_action() {
}}
class="input" />
</label>
{#if !journal?.cfg_json?.hide_btn_template}
<label class={field_card_class}>
<input
type="checkbox"
bind:checked={tmp_entry_obj.template}
onchange={() => {
handle_update_entry();
on_save();
}}
class="checkbox checkbox-primary" />
<div class="flex flex-col">
<span class="font-bold">Template</span>
<span class="text-xs opacity-60">
Mark this entry as a reusable template
</span>
</div>
</label>
{/if}
</div>
</section>
</div>
@@ -346,118 +388,28 @@ async function handle_admin_delete_action() {
<section class={panel_class}>
<h2 class={panel_title_class}>
<ShieldCheck size="1.2em" class="text-primary-500" />
Status & Security
Status
</h2>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<label class={field_card_class}>
<input
type="checkbox"
bind:checked={tmp_entry_obj.enable}
onchange={() => {
handle_update_entry();
on_save();
}}
class="checkbox checkbox-primary" />
<div class="flex flex-col">
<span class="font-bold">Enabled</span>
<span class="text-xs opacity-60"
>Allow access to this entry</span>
</div>
</label>
<label class={field_card_class}>
<input
type="checkbox"
bind:checked={tmp_entry_obj.hide}
onchange={() => {
handle_update_entry();
on_save();
}}
class="checkbox checkbox-primary" />
<div class="flex flex-col">
<span class="font-bold">Hidden</span>
<span class="text-xs opacity-60"
>Hide from standard lists</span>
</div>
</label>
<label class={field_card_class}>
<input
type="checkbox"
bind:checked={tmp_entry_obj.priority}
onchange={() => {
handle_update_entry();
on_save();
}}
class="checkbox checkbox-primary" />
<div class="flex flex-col">
<span class="font-bold">Priority Entry</span>
<span class="text-xs opacity-60"
>Star or pin to top</span>
</div>
</label>
<div class="bg-surface-500/5 border-surface-500/10 flex items-center justify-between rounded-lg border p-3">
<div class="flex flex-col">
<span class="text-sm font-bold"
>Sort Order</span>
<span class="text-xs opacity-60"
>Manual list position</span>
</div>
<div class="flex items-center gap-2">
<button
type="button"
class="btn-icon btn-icon-sm preset-tonal-surface"
onclick={() => {
tmp_entry_obj.sort =
(tmp_entry_obj.sort ?? 0) - 1;
{#if tmp_entry_obj.alert}
<label
class="label flex flex-col items-start gap-1 md:col-span-2">
<span class="text-sm font-bold opacity-70"
>Alert Message</span>
<textarea
bind:value={tmp_entry_obj.alert_msg}
class="textarea min-h-24 w-full"
placeholder="Optional alert or notice shown with the entry"
onchange={() => {
handle_update_entry();
on_save();
}}><Minus size="1em" /></button>
<span
class="w-8 text-center font-mono text-lg font-bold"
>{tmp_entry_obj.sort ?? 0}</span>
<button
type="button"
class="btn-icon btn-icon-sm preset-tonal-surface"
onclick={() => {
tmp_entry_obj.sort =
(tmp_entry_obj.sort ?? 0) + 1;
handle_update_entry();
on_save();
}}><Plus size="1em" /></button>
</div>
</div>
</div>
</section>
}}></textarea>
<span class="text-xs opacity-60">
Shown when Alert is enabled.
</span>
</label>
{/if}
<section class={panel_class}>
<h2 class={panel_title_class}>
<Fingerprint size="1.2em" class="text-primary-500" />
Privacy Flags
</h2>
<p class="text-xs opacity-60">
Visibility and audience controls for the entry itself.
</p>
<AE_Object_Flags
bind:obj={tmp_entry_obj}
show_labels={false}
on_toggle={() => {
handle_update_entry();
on_save();
}}
hide_alert={journal?.cfg_json?.hide_btn_alert}
hide_private={journal?.cfg_json?.hide_btn_private}
hide_public={journal?.cfg_json?.hide_btn_public}
hide_personal={journal?.cfg_json?.hide_btn_personal}
hide_professional={journal?.cfg_json
?.hide_btn_professional}
hide_template={journal?.cfg_json?.hide_btn_template} />
</section>
<section class={panel_class}>
<h2 class={panel_title_class}>
<CircleAlert size="1.2em" class="text-primary-500" />
Alerts & Messaging
</h2>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<label class={field_card_class}>
<input
type="checkbox"
@@ -469,22 +421,111 @@ async function handle_admin_delete_action() {
class="checkbox checkbox-primary" />
<div class="flex flex-col">
<span class="font-bold">Alert</span>
<span class="text-xs opacity-60"
>Flag this entry for emphasis</span>
<span class="text-xs opacity-60">
Action required or reminder.
</span>
</div>
</label>
<label class="label md:col-span-2">
<span class="text-sm font-bold opacity-70"
>Alert Message</span>
<textarea
bind:value={tmp_entry_obj.alert_msg}
class="textarea min-h-24"
placeholder="Optional note shown with the alert"
<label class={field_card_class}>
<input
type="checkbox"
bind:checked={tmp_entry_obj.priority}
onchange={() => {
handle_update_entry();
on_save();
}}></textarea>
}}
class="checkbox checkbox-primary" />
<div class="flex flex-col">
<span class="font-bold">Priority Entry</span>
<span class="text-xs opacity-60">
Pin this entry ahead of the standard order.
</span>
</div>
</label>
<label
class="bg-surface-500/5 border-surface-500/10 flex items-center justify-between rounded-lg border p-3">
<div class="flex flex-col">
<span class="text-sm font-bold"
>Sort Order</span>
<span class="text-xs opacity-60">
Lower numbers appear earlier in lists.
</span>
</div>
<input
type="number"
bind:value={tmp_entry_obj.sort}
class="input input-sm w-24 text-right" />
</label>
<label class={field_card_class}>
<input
type="checkbox"
bind:checked={tmp_entry_obj.hide}
onchange={() => {
handle_update_entry();
on_save();
}}
class="checkbox checkbox-primary" />
<div class="flex flex-col">
<span class="font-bold">Hidden</span>
<span class="text-xs opacity-60">
Hide from standard lists.
</span>
</div>
</label>
</div>
</section>
<section class={panel_class}>
<h2 class={panel_title_class}>
<FingerprintPattern
size="1.2em"
class="text-primary-500" />
Security and Privacy
</h2>
<div
class="border-surface-500/10 bg-surface-500/5 space-y-4 rounded-xl border p-4">
<label class="label flex flex-col items-start gap-1">
<span class="text-sm font-bold opacity-70"
>Passcode</span>
<input
type="password"
bind:value={tmp_entry_obj.passcode_hash}
class="input w-full"
placeholder="journal_entry.passcode_hash"
onchange={() => {
handle_update_entry();
on_save();
}} />
<span class="text-xs opacity-60">
Stored on the entry as passcode_hash.
</span>
</label>
<div class="space-y-2">
<p
class="text-xs font-semibold tracking-wider uppercase opacity-60">
Visibility and audience
</p>
<AE_Object_Flags
bind:obj={tmp_entry_obj}
show_labels={false}
on_toggle={() => {
handle_update_entry();
on_save();
}}
hide_alert={true}
hide_private={journal?.cfg_json
?.hide_btn_private}
hide_public={journal?.cfg_json?.hide_btn_public}
hide_personal={journal?.cfg_json
?.hide_btn_personal}
hide_professional={journal?.cfg_json
?.hide_btn_professional}
hide_template={true} />
</div>
</div>
</section>
@@ -513,12 +554,14 @@ async function handle_admin_delete_action() {
</section>
{/if}
{#if $ae_loc.trusted_access || $ae_loc.manager_access || $ae_loc.administrator_access}
<section class={panel_class}>
<h2 class={panel_title_class}>
<Settings size="1.2em" class="text-primary-500" />
Admin
</h2>
<details
class="border-surface-500/20 bg-surface-500/5 rounded-xl border shadow-sm">
<summary
class="border-surface-500/20 flex cursor-pointer items-center gap-2 border-b px-4 py-3 text-lg font-bold">
<Settings size="1.2em" class="text-primary-500" />
Admin
</summary>
<div class="space-y-4 p-4">
<p class="text-xs opacity-60">
Trusted access and above only. Notes are for staff
use; managers and admins see Delete while trusted
@@ -532,7 +575,7 @@ async function handle_admin_delete_action() {
<textarea
bind:value={tmp_entry_obj.notes}
class="textarea min-h-24"
placeholder="Rarely used staff/admin note"
placeholder="Rarely used special case admin/staff notes"
onchange={() => {
handle_update_entry();
on_save();
@@ -551,15 +594,22 @@ async function handle_admin_delete_action() {
class="checkbox checkbox-primary" />
<div class="flex flex-col">
<span class="font-bold">Enabled</span>
<span class="text-xs opacity-60"
>Default access state for this entry</span>
<span class="text-xs opacity-60">
Allow default access for AE object
type; essentially marked for
deletion.
</span>
</div>
</label>
<button
type="button"
class="btn btn-sm mx-auto inline-flex w-fit min-w-36 justify-center gap-2 px-4 font-bold {($ae_loc.manager_access || $ae_loc.administrator_access) ? 'preset-tonal-error hover:preset-filled-error-500' : 'preset-tonal-warning hover:preset-filled-warning-500'}"
title={($ae_loc.manager_access || $ae_loc.administrator_access)
class="btn btn-sm mx-auto inline-flex w-fit min-w-36 justify-center gap-2 px-4 font-bold {$ae_loc.manager_access ||
$ae_loc.administrator_access
? 'preset-tonal-error hover:preset-filled-error-500'
: 'preset-tonal-warning hover:preset-filled-warning-500'}"
title={$ae_loc.manager_access ||
$ae_loc.administrator_access
? 'Permanently delete this journal entry'
: 'Disable this journal entry instead of deleting it'}
onclick={handle_admin_delete_action}>
@@ -573,8 +623,8 @@ async function handle_admin_delete_action() {
</button>
</div>
</div>
</section>
{/if}
</div>
</details>
</div>
{:else if tab === 'json'}
<div class="h-full min-h-100">
@@ -605,4 +655,4 @@ async function handle_admin_delete_action() {
Done
</button>
{/snippet}
</Modal>
</Modal>

View File

@@ -54,12 +54,7 @@ describe('Journal Entry Search Filtering', () => {
});
expect(result?.length).toBe(4);
expect(result?.map((entry) => entry.id)).toEqual([
'4',
'3',
'2',
'1'
]);
expect(result?.map((entry) => entry.id)).toEqual(['4', '3', '2', '1']);
});
it('should filter by enabled and hidden status', () => {