Compare commits
2 Commits
a04d169bd8
...
08d2d804ff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08d2d804ff | ||
|
|
8f50259efe |
@@ -407,6 +407,7 @@ Cells: link strength (9/3/1/blank). Importance row = Σ(weight × strength).
|
|||||||
- **Asymmetric file index:** `addFile` deduped by SHA only, `registerUploadedFile` by PATH — so an edit (same path, new SHA) left a stale duplicate entry. **Fixed** — `addFile` now dedups by SHA *and* path (a path is unique), with a regression test in `userRepo.store.spec.ts`. (Full bidirectional index C3 still the longer-term direction.)
|
- **Asymmetric file index:** `addFile` deduped by SHA only, `registerUploadedFile` by PATH — so an edit (same path, new SHA) left a stale duplicate entry. **Fixed** — `addFile` now dedups by SHA *and* path (a path is unique), with a regression test in `userRepo.store.spec.ts`. (Full bidirectional index C3 still the longer-term direction.)
|
||||||
- **ADR-0001 over-absolute:** it called SHA-keyed references "a fragility". **Refined** by ADR-0002 into two intentional modes (Live/Snapshot).
|
- **ADR-0001 over-absolute:** it called SHA-keyed references "a fragility". **Refined** by ADR-0002 into two intentional modes (Live/Snapshot).
|
||||||
- **`CLAUDE.md` "edit history tracking"** mislabels the visited-repos History; no edit-history feature exists. (Flagged in CONTEXT.md.)
|
- **`CLAUDE.md` "edit history tracking"** mislabels the visited-repos History; no edit-history feature exists. (Flagged in CONTEXT.md.)
|
||||||
|
- **Snapshot cache not immutable:** editing writes new content under the *viewed* (old) SHA's cache key (`prepareNoteCache` keys by the viewed sha), so a re-opened old-SHA snapshot can serve new content from cache. **Decided fix (ADR-0002):** key the immutable store by the content's *own* SHA (write-once), keep the Path key as the latest pointer. *(Not yet implemented — the C3/C4 cache slice; unlocks the snapshot banner.)*
|
||||||
- **Naming drift carried from the language session:** `Flux`→`Igarapé`, `HistoricNotes`→`Index`, `Repetition`→`Card`, `PublicNote*`→`PublishedNote*`, `useNotes()` returns Files not Notes. (Tracked in CONTEXT.md "Flagged ambiguities".)
|
- **Naming drift carried from the language session:** `Flux`→`Igarapé`, `HistoricNotes`→`Index`, `Repetition`→`Card`, `PublicNote*`→`PublishedNote*`, `useNotes()` returns Files not Notes. (Tracked in CONTEXT.md "Flagged ambiguities".)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ This refines ADR-0001, which framed SHA-keyed references as "a fragility." That
|
|||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- The system **pre-caches** aggressively (content is cached by both SHA and Path on every fetch / freshness pull) so the pinned version is usually present, including offline.
|
- The system **pre-caches** aggressively (content is cached by both SHA and Path on every fetch / freshness pull) so the pinned version is usually present, including offline.
|
||||||
|
- **Cache shape (C3/C4):** content is cached under two keys — its own **content SHA** (write-once, immutable → the snapshot store) and its **Path** (overwritten with the latest content → the live pointer). Both hold the full content, so the latest survives even if a SHA entry is evicted, maximizing offline availability for any version already encountered. (Today the code violates this by writing edited content under the *viewed* old SHA key; the fix is to write under the content's *new* SHA and never overwrite an existing SHA entry.)
|
||||||
- When a Snapshot reference's pinned content is genuinely unavailable (never-fetched + offline — common on a flaky mobile connection), the system **falls back to the most up-to-date cached version and shows a banner** disclosing "this is the latest available, not the exact shared version." Integrity is preserved by **disclosure, not refusal**: the reader is never silently shown different content, but is also never dead-ended on the metro. (This chooses graceful continuity over a hard "unavailable" stop, given mobile is the primary read context.)
|
- When a Snapshot reference's pinned content is genuinely unavailable (never-fetched + offline — common on a flaky mobile connection), the system **falls back to the most up-to-date cached version and shows a banner** disclosing "this is the latest available, not the exact shared version." Integrity is preserved by **disclosure, not refusal**: the reader is never silently shown different content, but is also never dead-ended on the metro. (This chooses graceful continuity over a hard "unavailable" stop, given mobile is the primary read context.)
|
||||||
|
|||||||
29
src/components/NoteState.spec.ts
Normal file
29
src/components/NoteState.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { mount } from "@vue/test-utils"
|
||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
import NoteState from "./NoteState.vue"
|
||||||
|
|
||||||
|
describe("NoteState", () => {
|
||||||
|
it("shows a spinner while loading and no retry", () => {
|
||||||
|
const wrapper = mount(NoteState, { props: { status: "loading" } })
|
||||||
|
|
||||||
|
expect(wrapper.find(".loading-spinner").exists()).toBe(true)
|
||||||
|
expect(wrapper.find(".note-state-retry").exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("shows a message and retry button when failed", () => {
|
||||||
|
const wrapper = mount(NoteState, { props: { status: "failed" } })
|
||||||
|
|
||||||
|
expect(wrapper.find(".loading-spinner").exists()).toBe(false)
|
||||||
|
expect(wrapper.text()).toContain("Couldn't load this note")
|
||||||
|
expect(wrapper.find(".note-state-retry").exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("emits retry when the retry button is clicked", async () => {
|
||||||
|
const wrapper = mount(NoteState, { props: { status: "failed" } })
|
||||||
|
|
||||||
|
await wrapper.find(".note-state-retry").trigger("click")
|
||||||
|
|
||||||
|
expect(wrapper.emitted("retry")).toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
48
src/components/NoteState.vue
Normal file
48
src/components/NoteState.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{ status: "loading" | "failed" }>()
|
||||||
|
defineEmits<{ retry: [] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="note-state">
|
||||||
|
<span
|
||||||
|
v-if="status === 'loading'"
|
||||||
|
class="loading loading-spinner loading-md"
|
||||||
|
aria-label="Loading note"
|
||||||
|
></span>
|
||||||
|
<template v-else>
|
||||||
|
<p class="note-state-message">Couldn't load this note.</p>
|
||||||
|
<button type="button" class="note-state-retry" @click="$emit('retry')">
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.note-state {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
color: var(--color-base-content);
|
||||||
|
opacity: 0.55;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-state-message {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-state-retry {
|
||||||
|
padding: 0.25rem 0.85rem;
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -40,6 +40,10 @@ const EditNote = defineAsyncComponent(
|
|||||||
() => import("@/modules/note/components/EditNote.vue")
|
() => import("@/modules/note/components/EditNote.vue")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const NoteState = defineAsyncComponent(
|
||||||
|
() => import("@/components/NoteState.vue")
|
||||||
|
)
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
user: string
|
user: string
|
||||||
repo: string
|
repo: string
|
||||||
@@ -154,10 +158,21 @@ const {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const conflictOpen = ref(false)
|
const conflictOpen = ref(false)
|
||||||
|
const loadStatus = ref<"loading" | "ready" | "failed">("loading")
|
||||||
|
|
||||||
onMounted(async () => {
|
const loadNote = async () => {
|
||||||
initialRawContent.value = await getRawContent()
|
loadStatus.value = "loading"
|
||||||
})
|
const raw = await getRawContent()
|
||||||
|
if (raw === null) {
|
||||||
|
loadStatus.value = "failed"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rawContent.value = raw
|
||||||
|
initialRawContent.value = raw
|
||||||
|
loadStatus.value = "ready"
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadNote)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
path,
|
path,
|
||||||
@@ -412,11 +427,18 @@ const onBadgeClick = async () => {
|
|||||||
<div v-if="mode === 'edit' && isMarkdown" class="edit">
|
<div v-if="mode === 'edit' && isMarkdown" class="edit">
|
||||||
<edit-note :key="editKey" v-model="rawContent" />
|
<edit-note :key="editKey" v-model="rawContent" />
|
||||||
</div>
|
</div>
|
||||||
|
<template v-else-if="mode === 'read'">
|
||||||
<div
|
<div
|
||||||
v-if="mode === 'read'"
|
v-if="displayedContent"
|
||||||
class="note-content"
|
class="note-content"
|
||||||
v-html="displayedContent"
|
v-html="displayedContent"
|
||||||
></div>
|
></div>
|
||||||
|
<note-state
|
||||||
|
v-else
|
||||||
|
:status="loadStatus === 'failed' ? 'failed' : 'loading'"
|
||||||
|
@retry="loadNote"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</section>
|
</section>
|
||||||
<linked-notes v-if="hasBacklinks && content" :sha="sha" />
|
<linked-notes v-if="hasBacklinks && content" :sha="sha" />
|
||||||
<note-conflict-modal
|
<note-conflict-modal
|
||||||
|
|||||||
Reference in New Issue
Block a user