feat: resource caps, Saved Messages, author walls, docs for node bring-up

Node flags (cmd/node/main.go):
  --max-cpu / --max-ram-mb — Go runtime caps (GOMAXPROCS / GOMEMLIMIT)
  --feed-disk-limit-mb — hard 507 refusal for new post bodies over quota
  --chain-disk-limit-mb — advisory watcher (can't reject blocks without
  breaking consensus; logs WARN every minute)

Client — Saved Messages (self-chat):
  - Auto-created on sign-in, pinned top of chat list, blue bookmark avatar
  - Send short-circuits the relay (no encrypt, no fee, no mailbox hop)
  - Empty state rendered outside inverted FlatList — fixes the mirrored
    "say hi…" on Android RTL-aware layout builds
  - PostCard shows "You" for own posts instead of the self-contact alias

Client — user walls:
  - New route /(app)/feed/author/[pub] with infinite-scroll via
    `created_at` cursor and pull-to-refresh
  - Profile screen gains "View posts" button (universal) next to
    "Open chat" (contact-only)

Feed pipeline:
  - Bump client JPEG quality 0.5 → 0.75 to match server scrubber (Q=75),
    so a 60 KiB compose doesn't balloon past 256 KiB after server re-encode
  - ErrPostTooLarge now wraps with the actual size vs cap, errors.Is
    preserved in the HTTP layer
  - FeedMailbox quota + DiskUsage surface — supports new CLI flag

README:
  - Step-by-step "first node / joiner" section on the landing page,
    full flag tables incl. the new resource-cap group, minimal
    checklists for open/private/low-end deployments
This commit is contained in:
vsecoder
2026-04-19 13:14:47 +03:00
parent e6f3d2bcf8
commit a75cbcd224
18 changed files with 870 additions and 102 deletions

View File

@@ -97,24 +97,35 @@ type FeedPost struct {
// ErrPostTooLarge is returned by Store when the post body exceeds MaxPostBodySize.
var ErrPostTooLarge = errors.New("post body exceeds maximum allowed size")
// ErrFeedQuotaExceeded is returned by Store when the on-disk footprint
// (LSM + value log) plus the incoming post would exceed the operator-set
// disk quota. Ops set this via --feed-disk-limit-mb. Zero = unlimited.
var ErrFeedQuotaExceeded = errors.New("feed mailbox disk quota exceeded")
// FeedMailbox stores feed post bodies.
type FeedMailbox struct {
db *badger.DB
ttl time.Duration
db *badger.DB
ttl time.Duration
quotaBytes int64 // 0 = unlimited
}
// NewFeedMailbox wraps an already-open Badger DB. TTL controls how long
// post bodies live before auto-eviction (on-chain metadata persists
// forever independently).
func NewFeedMailbox(db *badger.DB, ttl time.Duration) *FeedMailbox {
// forever independently). quotaBytes caps the on-disk footprint; 0 or
// negative means unlimited.
func NewFeedMailbox(db *badger.DB, ttl time.Duration, quotaBytes int64) *FeedMailbox {
if ttl <= 0 {
ttl = time.Duration(FeedPostDefaultTTLDays) * 24 * time.Hour
}
return &FeedMailbox{db: db, ttl: ttl}
if quotaBytes < 0 {
quotaBytes = 0
}
return &FeedMailbox{db: db, ttl: ttl, quotaBytes: quotaBytes}
}
// OpenFeedMailbox opens (or creates) a dedicated BadgerDB at dbPath.
func OpenFeedMailbox(dbPath string, ttl time.Duration) (*FeedMailbox, error) {
// quotaBytes caps the total on-disk footprint (LSM + vlog); 0 = unlimited.
func OpenFeedMailbox(dbPath string, ttl time.Duration, quotaBytes int64) (*FeedMailbox, error) {
opts := badger.DefaultOptions(dbPath).
WithLogger(nil).
WithValueLogFileSize(128 << 20).
@@ -124,9 +135,19 @@ func OpenFeedMailbox(dbPath string, ttl time.Duration) (*FeedMailbox, error) {
if err != nil {
return nil, fmt.Errorf("open feed mailbox db: %w", err)
}
return NewFeedMailbox(db, ttl), nil
return NewFeedMailbox(db, ttl, quotaBytes), nil
}
// DiskUsage returns the current on-disk footprint (LSM + value log) in
// bytes. Cheap — Badger tracks these counters internally.
func (fm *FeedMailbox) DiskUsage() int64 {
lsm, vlog := fm.db.Size()
return lsm + vlog
}
// Quota returns the configured disk quota in bytes. 0 = unlimited.
func (fm *FeedMailbox) Quota() int64 { return fm.quotaBytes }
// Close releases the underlying Badger handle.
func (fm *FeedMailbox) Close() error { return fm.db.Close() }
@@ -139,7 +160,23 @@ func (fm *FeedMailbox) Close() error { return fm.db.Close() }
func (fm *FeedMailbox) Store(post *FeedPost, createdAt int64) ([]string, error) {
size := estimatePostSize(post)
if size > MaxPostBodySize {
return nil, ErrPostTooLarge
// Wrap the sentinel so the HTTP layer can still errors.Is() on it
// while the operator / client sees the actual offending numbers.
// This catches the common case where the client's pre-scrub
// estimate is below the cap but the server re-encode (quality=75
// JPEG) inflates past it.
return nil, fmt.Errorf("%w: size %d > max %d (after server scrub)",
ErrPostTooLarge, size, MaxPostBodySize)
}
// Disk quota: refuse new bodies once we're already over the cap.
// `size` is a post-body estimate, not the exact BadgerDB write-amp
// cost; we accept that slack — the goal is a coarse guard-rail so
// an operator's disk doesn't blow up unnoticed. Exceeding nodes
// still serve existing posts; only new Store() calls are refused.
if fm.quotaBytes > 0 {
if fm.DiskUsage()+int64(size) > fm.quotaBytes {
return nil, ErrFeedQuotaExceeded
}
}
post.CreatedAt = createdAt