Skip to content

feat: עיצוב מחדש של מסך הגדרות הניהול + captain-definition - #23

Open
27180781 wants to merge 161 commits into
NetFree-Community:masterfrom
27180781:master
Open

27180781 wants to merge 161 commits into
NetFree-Community:masterfrom
27180781:master

Conversation

@27180781

@27180781 27180781 commented May 7, 2026

Copy link
Copy Markdown

Summary

  • שכתוב מסך הגדרות הניהול: במקום טבלת key/value גנרית, ההגדרות מאורגנות בקטגוריות עם תוויות והסברים בעברית, עם רכיבים מתאימים לכל סוג שדה (toggle לבוליאני, מספר, סיסמה, textarea וכו').
  • קטגוריות: כללי, הזדהות ואבטחה, מונה צפיות, פרסומות, וובהוק, התראות דחיפה (FCM), חשבון שירות FCM, החלפות טקסט אוטומטיות.
  • תוספות נוחות: רשימה דינמית לכללי regex-replace (שדות pattern/replace נפרדים), הדבקת JSON של Service Account שמפצל אוטומטית לכל שדות fcm_json_*, ומקטע "הגדרות מתקדמות" מתקפל בתחתית שמאפשר עדיין הוספת key/value חופשי (גם מציג הגדרות לא מוכרות שכבר קיימות).
  • תאימות לאחור: אין שינוי ב-API. הקומפוננטה ממירה את כל הערכים בחזרה למבנה [{key, value}] בעת שמירה (בוליאנים נשמרים כ-'1', regex כ-pattern#replace).
  • הוספת קובץ captain-definition להגדרת פריסה ב-CapRover.

Files changed

  • frontend/src/app/components/admin/settings/settings.schema.ts (חדש) — schema עם קטגוריות, סוגי שדות, ותיאורים בעברית.
  • frontend/src/app/components/admin/settings/settings.component.ts — נכתב מחדש לטעון/לשמור לפי ה-schema.
  • frontend/src/app/components/admin/settings/settings.component.html — UI חדש לחלוטין.
  • frontend/src/app/components/admin/settings/settings.component.scss — סגנונות חדשים.
  • captain-definition — קובץ פריסה.

Test plan

  • להיכנס לממשק הניהול ולוודא שכל ההגדרות הקיימות נטענות נכון לשדות החדשים.
  • להפעיל/לכבות toggles ולוודא שהערך נשמר נכון בבקאנד (1/לא קיים).
  • להזין כללי regex-replace ולוודא שהם נשמרים בפורמט pattern#replace ופועלים בפרסום הודעות.
  • להדביק JSON של Service Account ולוודא שכל שדות fcm_json_* מתמלאים.
  • להוסיף הגדרה דרך "הגדרות מתקדמות" ולוודא שהיא נשמרת ונטענת בחזרה לאזור המתקדם.
  • לוודא שהגדרות לא מוכרות שכבר קיימות במערכת מופיעות באזור "הגדרות מתקדמות".

🤖 Generated with Claude Code

27180781 and others added 30 commits May 7, 2026 04:56
Replace the generic key/value table with a structured form: settings are
grouped by category (general, auth, ads, webhook, notifications, FCM),
each with Hebrew labels and descriptions. Booleans are toggles, regex
rules have separate pattern/replace fields, and a paste-JSON helper
auto-fills FCM service account fields. A collapsible "advanced" section
preserves manual key/value entry for unknown settings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a dedicated admin tab "שילוב פרסומות ממגנט" where admins paste an
HTML/JS embed snippet from the Magnet ad platform. Ads render between
chat messages according to configurable rules: either every N messages
(with optional minimum time gap) or every N seconds (with optional
minimum new-messages gap).

Each ad slot is lazy-loaded with IntersectionObserver — the external
embed only fires when the user scrolls near it, and re-fires on every
re-mount so each viewer (and each scroll) gets a live ad. If the embed
produces no DOM content within 5 seconds, the slot collapses silently.

Backend exposes a new public GET /api/ads/magnet endpoint and persists
the seven magnet_* keys in the existing settings:list store.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a publisher API key field and a stats viewer in the Magnet admin tab.
A new admin-protected endpoint GET /api/admin/magnet/stats proxies the
request to Magnet's publisher-stats function so the API key never reaches
the browser. The stats panel shows clicks and earnings (today / week /
month) in two columns with currency formatting, displays the site domain,
and has a refresh button with a last-updated timestamp. Errors from
Magnet (400 invalid key, 404 unapproved site) are surfaced in Hebrew.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add defensive fallbacks so a regression in the Magnet ad-injection logic
can never prevent regular chat messages from rendering:

- rebuildItems() now wraps buildItems() in try/catch and falls back to
  plain message items if it throws or returns an empty list while there
  are messages.
- Template renders directly from messages[] when items[] is empty,
  bypassing the ad-injection layer entirely as a safety net.
- ngOnInit catches a rejected loadSettings() promise so it cannot block
  the subsequent rebuildItems call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous approach replaced the @for over messages with a @for over a
new ChatItem[] union type. That introduced a subtle regression where
messages stopped rendering for some users. Restore the original
@for (message of messages; track message) loop and inject the magnet ad
slot as a sibling <nb-list-item> right after each message that should
have an ad — driven by an adSlotsAfter Set<number> of message IDs.

Service no longer builds an items array; it returns the Set directly via
computeAdSlots(). The chat component owns adSlotsAfter and recomputes it
on every message-list mutation. If computation throws, the Set stays
empty and messages render unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dering

Despite restoring the @for messages template loop, deployed builds were
still rendering an empty chat list with no messages, no loading spinner,
and no "no messages" placeholder — a state inconsistent with what the
template should produce. To unblock users, fully revert chat.component.ts
and chat.component.html to the pre-magnet snapshot. The magnet ads
service, slot component, admin tab, and backend endpoints remain in
place; ad injection into the chat list will be re-introduced separately
once the regression is understood.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 1 of incremental re-introduction: add the MagnetAdsService
injection, the adSlotsAfter Set, the rebuildItems() helper, and the
ngOnInit loadSettings() call. The template is left unchanged from the
known-working revert. If the chat still renders, we know the regression
came from the template @if injection rather than the service wiring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 2 of incremental re-introduction: add the @if (adSlotsAfter.has(...))
template block, but render a plain <div> placeholder instead of the real
<app-magnet-ad-slot> component. If messages still render with this in
place, the regression was tied to the magnet-ad-slot component (DI,
ngAfterViewInit, IntersectionObserver, etc.) rather than to the @if
template structure itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 3 of incremental re-introduction: instead of adding a sibling
<nb-list-item> after the message item, place the ad placeholder inside
the same <nb-list-item> as the message. This avoids creating a second
projected list-item child of <nb-list> via @if, which appears to be
what blocks rendering — Nebular's <nb-list> may rely on a content-child
query that does not survive a conditional second item.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that the regression is understood (creating a sibling <nb-list-item>
via @if inside @for breaks Nebular's <nb-list> rendering), wire the real
<app-magnet-ad-slot> component back in — but place it inside the
existing <nb-list-item> rather than as a sibling. This keeps every
iteration of @for producing exactly one <nb-list-item>, which is what
<nb-list> expects.

Also re-add MagnetAdSlotComponent to ChatComponent's imports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wrap the magnet ad slot in the same visual structure as a regular admin
message: channel logo on the side, channel name + 'פרסומת' label
above, and the embed itself rendered inside the same .message-card
bubble used for chat messages. Inject ChatService into the slot
component to read channelInfo for the logo and name.

The collapsed state now toggles a CSS class on the wrapper (display:
none) instead of rebuilding the DOM, which keeps the #host ViewChild
stable across renders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a new "אנליטיקס וטראקינג" category in the admin settings UI with a
textarea field for pasting any HTML/JS analytics snippet (Google
Analytics, GTM, Meta Pixel, etc.). The backend stores it under the
analytics_head settings key and serveSpaFile injects it into the
served index.html immediately before the closing </head> tag, so the
snippet runs on every page load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update SET.md with the changes introduced over this work session:
- The new visual settings UI with categorized cards.
- The analytics_head setting and its head injection.
- The full Magnet ad integration (frequency rules, lazy loading, ad
  bubble rendering, stats panel) and the architectural note about why
  ads are nested inside the existing <nb-list-item> rather than as
  siblings.
- The new /api/ads/magnet and /api/admin/magnet/stats endpoints.
- A complete refreshed settings table including all magnet_* keys and
  analytics_head.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rewrite privileges.go with new GlobalRole/ChannelRole system (SuperAdmin, Owner, Moderator, Writer)
- Add channels.go with ChannelData, ChannelFeatures, and channel CRUD handlers
- Add channelMiddleware to inject channel into request context
- Channel owners can manage their users; super admin controls everything

https://claude.ai/code/session_015yWNYaVjk7ozyguKLA8vvx
- Replace old Privileges map with GlobalRole and ChannelRoles fields
- Update getUser to populate GlobalRole and ChannelRoles from privilegesUsers
- Update registeringEmail to accept slug for per-channel email tracking
- Remove checkPrivilege (replaced by isSuperAdmin/hasChannelRole)

https://claude.ai/code/session_015yWNYaVjk7ozyguKLA8vvx
- db.go: all DB functions now channel-scoped (slug parameter), added channel CRUD
  (dbCreateChannel, dbGetChannel, dbListChannels, dbDeleteChannel, dbSetChannelFeatures)
  Settings split: per-channel (channel:{slug}:settings) + global (global:settings)
- messages.go: slug from context, SSE subscribes to events:{slug}
- settings.go: per-channel getSettings/setSettings + getGlobalSettings/setGlobalSettings
- statistics.go: per-channel SSE counters via sync.Map
- reactions.go: per-channel emojis loaded from DB per request
- report.go, scheduled.go: channel-scoped DB calls
- notifications.go: FCM stays global, subscriptions are per-channel
- ads.go, webhook.go: load per-channel config via getChannelConfig()
- api.go: API key validated from per-channel settings
- files.go: file URL includes channel slug, max size from per-channel config
- channelInfo.go: features sourced from channel context
- main.go: full routing rewrite - /api/channel/{slug}/... and /api/super-admin/...

https://claude.ai/code/session_015yWNYaVjk7ozyguKLA8vvx
- ChannelFeatures: add MagnetLockedByAdmin bool (set by super admin only)
- db.go: add GlobalMagnetConfig struct + dbGetGlobalMagnetConfig/dbSetGlobalMagnetConfig
  stored at global:magnet:config
- ads.go: getMagnetAdsSettings now checks if channel is locked (LockAll, LockedChannels
  list, or MagnetLockedByAdmin flag) and returns global config instead of channel config
- ads.go: add getGlobalMagnetConfig/setGlobalMagnetConfig handlers for super admin
- ads.go: syncMagnetLockFlags goroutine syncs MagnetLockedByAdmin on all channels
  when super admin saves global config
- getMagnetStats now reads API key from global magnet config
- main.go: add GET/POST /api/super-admin/magnet/config routes

https://claude.ai/code/session_015yWNYaVjk7ozyguKLA8vvx
…s Magnet)

- ChannelFeatures: add AdsLockedByAdmin bool
- db.go: add GlobalAdsConfig struct + dbGetGlobalAdsConfig/dbSetGlobalAdsConfig
  stored at global:ads:config (src, width, lockAll, lockedChannels)
- ads.go: getAdsSettings checks if channel is locked and returns global src/width
- ads.go: add getGlobalAdsConfig/setGlobalAdsConfig handlers + syncAdsLockFlags goroutine
- main.go: add GET/POST /api/super-admin/ads/config routes

https://claude.ai/code/session_015yWNYaVjk7ozyguKLA8vvx
- user.model.ts: add globalRole, channelRoles, publicName fields
- super-admin.service.ts: all super admin API calls
- super-admin.guard.ts: route guard (globalRole === super_admin only)
- super-admin/channels: channels list, channel features, channel users components
- super-admin/global-ads: global ads config component (partial)

https://claude.ai/code/session_015yWNYaVjk7ozyguKLA8vvx
- super-admin-panel.component: main shell with sidebar menu (ערוצים, פרסומות iframe,
  פרסומות מגנט, משתמשים, הגדרות גלובליות, סטטיסטיקות)
- channels-list: table with create/edit-features/manage-users/delete actions
- channel-features: all 12 feature toggles with Hebrew labels (incl. lock indicators)
- channel-users: add/remove/change-role per channel, save button
- global-ads: src, width, lockAll toggle, locked channels tag input
- global-magnet: full magnet config + frequency settings + lock rules
- global-users: read-only overview of all users with role chips
- global-settings: key-value FCM/VAPID settings editor
- statistics: reset peak connections + magnet stats display
- app.routes.ts: add /super-admin route with SuperAdminGuard
- channel-header: add "פאנל מנהל-על" link for super admins
- channel.component: show input footer for super_admin + channel role holders

https://claude.ai/code/session_015yWNYaVjk7ozyguKLA8vvx
- storage.go: R2 client init (aws-sdk-go-v2/s3), upload, exists-check, download helpers
  Reads env vars: R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY,
  R2_BUCKET_NAME, R2_PUBLIC_URL (optional)
- files.go: file metadata moved to Redis (key: file:{id}), YAML fallback for legacy files
  If R2 configured: uploads to R2, serves via redirect (public URL) or proxy
  If R2 not configured: local disk at /app/files/ (backward compatible)
  Deduplication by SHA-256 hash preserved for both R2 and local
- main.go: call initR2() on startup
- sample.env: document R2 env vars
- go.mod/go.sum: add aws-sdk-go-v2 dependencies

https://claude.ai/code/session_015yWNYaVjk7ozyguKLA8vvx
…ression

- Backend: storage quota tracking with per-channel and global defaults
- Backend: auto-cleanup oldest files when quota is near full
- Backend: TinyPNG API integration — compresses PNG/JPEG/WebP before upload
- Frontend (admin): channel storage panel with usage bar, warnings, auto-cleanup toggle
- Frontend (admin): TinyPNG API key field in settings (storage category)
- Frontend (super-admin): global storage quota config panel
- Frontend (super-admin): per-channel storage view with quota override and usage bar
- Frontend (super-admin): "אחסון" button in channels list to open per-channel storage
- Super admin service: added getGlobalStorageConfig/setGlobalStorageConfig/getChannelStorage/setChannelStorage

https://claude.ai/code/session_015yWNYaVjk7ozyguKLA8vvx
- Public landing page at '/' with hero, how-it-works, features, and request form
- Route '/' is now the landing page; '/channel' is the auth-guarded channel app
- Visitors fill out a form (name, email, desired slug, description)
- Backend stores requests in Redis, exposed via GET /api/super-admin/channel-requests
- Super admin can approve (auto-creates channel + assigns owner) or reject requests
- Approval shows slug, owner email, and the channel link to copy and send manually
- Login redirects super_admin → /super-admin, others → /channel
- Logout navigates back to the landing page
- Super admin panel: new 'בקשות לערוצים' menu item with inline approve/reject table

https://claude.ai/code/session_015yWNYaVjk7ozyguKLA8vvx
Security (data isolation):
- serveFile: verify meta.ChannelSlug matches URL slug — blocks cross-channel file access
- addNewPost (/import): validate slug against slugRegex to prevent key injection

Performance (50+ concurrent channels):
- Redis pool: PoolSize=100, MinIdleConns=10, retries, timeouts (was default 10)
- dbDeleteChannel: replace O(keyspace) SCAN with targeted pipeline using known
  key names + message IDs from sorted set — O(messages) instead of O(total keys)
- Scheduled messages: maintain sorted set "scheduled:due_channels" (score=next due time)
  so the goroutine only wakes channels with pending messages, not all channels every minute
- Lua getMessageRange: add max_scan_rounds=20 guard to prevent unbounded loops on sparse data

Rate limiting:
- File uploads: 30/min per channel (burst=10) via golang.org/x/time/rate
  returns HTTP 429 when exceeded

https://claude.ai/code/session_015yWNYaVjk7ozyguKLA8vvx
Redis Streams (horizontal scaling):
- Replace pub/sub with Redis Streams for SSE events
  * publishEvent() uses XADD (MaxLen~1000, auto-trim)
  * getEvents() uses XREAD BLOCK with 5s timeout loop
  * Supports Last-Event-ID header: clients reconnect without missing events
  * Multiple backend instances can all serve SSE from the same stream
- Stream key: channel:{slug}:events (consistent naming convention)
- Stream keys added to dbDeleteChannel cleanup list

Redis Universal Client (Cluster/Sentinel support):
- rdb changed from *redis.Client to redis.UniversalClient
- NewUniversalClient: single addr = normal, multiple REDIS_ADDRS = cluster,
  REDIS_MASTER set = Sentinel — zero code changes needed to scale up

Pre-signed R2 URLs (backend off the file-serving path):
- r2PresignURL() generates 1-hour signed URLs via s3.NewPresignClient
- serveFile: public bucket → CDN redirect, private bucket → pre-signed URL,
  only falls back to backend proxy if presigning fails

HTTP ETags for message caching:
- touchLastModified() stores nano-timestamp on every message write/edit/delete
- getMessages returns ETag header; returns 304 Not Modified when client is current
- Eliminates redundant Lua script executions for unchanged channels

Per-user upload rate limiting:
- Rate limiter key is now userEmail:channelSlug instead of just slug
- Prevents one user from exhausting the channel's upload budget
- 30 uploads/min per user per channel (burst 10)

https://claude.ai/code/session_015yWNYaVjk7ozyguKLA8vvx
claude and others added 30 commits September 24, 2026 19:51
- User list: hiding every super-admin row made a role an owner granted to the
  operator's email vanish after saving, which looked like a failed save and
  confirmed the address was the operator's. Only the operator's owner rows are
  hidden now; an owner can never grant owner, so those were never theirs.
- Author ids: a record created after its user's last login (self-service
  creation, an invitation) had no Google id, so posts written in that window
  could not be recognised if the author was later made an operator. addMessage
  now records the id when the record lacks it.
- Chat: the operator's own post raised the new-messages dot, since its
  authorId is the shared stand-in rather than their own id.
- Tests: the report test checks the stored hash, not only the re-labelled
  read; it resets the report limiter so repeated runs pass; and simulated
  sessions no longer leave 30-day keys in Redis.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCyKp95KMz4rW3E7fRRmb3
…settings

- /messages no longer answers 304 while view counting is on: the
  short-circuit skipped the per-message increment, so repeat visitors to
  a quiet channel were never counted and saw their first visit's numbers.
- Push subscribers are read with SSCAN in pages instead of one SMEMBERS
  reply holding every device that ever subscribed; registration is rate
  limited per client so one user cannot grow the set without bound.
- A message hash and its index entry are written in one transaction so a
  failure between them cannot leave an orphan, unlisted hash.
- Global settings saves are announced over pub/sub and reloaded on every
  replica (with a periodic refresh as backstop), like the roles map.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GmkznW6GgQMoCPReH76A6
- channelMiddleware and the super-admin write handlers refuse slugs that
  can never be a channel (a ':' or a slash, over 64 chars) instead of the
  creation regex, so channels created before that regex existed stay
  reachable while "x:messages:5" no longer resolves as a phantom channel.
- The login limiter allows twenty attempts at once and one every two
  seconds per address, enough for many people behind one shared address.
- Restore canModifyMessage's doc comment; simplify the claim read in getUser.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GmkznW6GgQMoCPReH76A6
- Pre-signed R2 URLs carry the Content-Disposition, so a private-bucket
  download keeps its name and extension (the header on the 302 was
  discarded by the browser and the file saved as a bare hash).
- File type is sniffed on more than 512 bytes, so Office documents are
  typed as documents rather than application/zip.
- Auto-cleanup never evicts the channel's logo, which as the oldest
  upload was the first to go.
- Uploads are refused before the body is read when the channel is full
  with no auto-cleanup; at most four are buffered at once process-wide,
  the rest get a 503 with Retry-After.
- Local blobs are written to a temp name and renamed into place, so a
  crash mid-write cannot leave a truncated blob at the hash path.
- R2_PUBLIC_URL is trimmed of trailing slashes and the sample placeholder
  is ignored; sample.env ships it commented out.
- The composer attaches every picked file (the input advertised multiple
  but only the first was uploaded) and resets the input so the same file
  can be picked again; upload refusals (full, rate limited, disabled,
  busy) each get their own Hebrew message on both upload forms.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GmkznW6GgQMoCPReH76A6
Backend
- Channel features: a WATCH/MULTI updater; the ads/magnet lock sync and
  the super-admin features form both go through it, the sync is
  serialised and logs failures, and the form keeps the stored lock
  mirrors instead of replaying a stale pair. A channel disabled while a
  lock sync ran no longer comes back enabled.
- ad-iframe-src is accepted only as an absolute http(s) URL; a
  javascript: value bound to an iframe ran in the app's origin.
- dbListChannels is pipelined in batches instead of two round trips per
  channel.
- Channel deletion aborts on a failed index read instead of deleting the
  indexes and orphaning every message, report and file blob behind them.
- Scheduled-message saves report a failed due-channels index write.
- Peak SSE connections: no record is a zero value, not 01/01/1970.
- SSE counter refresher can no longer drop a connection that arrived
  while it withdrew an idle counter.
- Uploads: a failed file-index write releases the file and fails the
  upload; auto-cleanup prunes index entries whose file is gone or
  tombstoned, so dead entries stop filling its 200-oldest window.
- Support: text limits count characters like the form's maxlength; the
  anonymous ticket token travels in X-Ticket-Token instead of the query
  string the request log records; IPv6 remote addresses keep their last
  hextet in the rate-limit key.

Frontend
- The magnet snippet runs in a sandboxed srcdoc iframe (opaque origin)
  sized by a postMessage from the frame, instead of being injected into
  the app document with its scripts re-created.
- The ads iframe renders only http(s) sources and carries a sandbox.
- Support box tells a full thread apart from a closed ticket.
- Super-admin statistics flatten nested magnet stats and the reset
  prompt says the connection history is wiped too.
- Owner statistics show "no peak yet" instead of a 1970 date.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GmkznW6GgQMoCPReH76A6
…gration guards

Reactions are toggled and summed in one Lua script, so two simultaneous
reactions no longer persist a stale count. Anonymous support tickets are no
longer indexed under the typed e-mail (a visitor could plant a thread in any
user's "my tickets" and read the replies), signed-in tickets get no access
token, and session access applies only to authenticated tickets.

Channel deletion publishes a channel-deleted event before removing the
stream, holds the scheduler claim, refuses implausible slugs and reports
files it could not release. The scheduler claims a channel per tick, defers
disabled channels, forgets deleted ones, whitelists the stored fields and
records the scheduler's name instead of the literal "Scheduled".

Migrations take a per-replica lock, get a 2-minute budget each, treat an
exhausted budget as "not applied" in every loop, and the R2 copy remembers
copied hashes in migrations:r2-copied so retries are O(remaining).

Also: settings validated on save (webhook_url/ad-iframe-src http(s), regex
rules compile, max_file_size 1..512), role grants must look like an e-mail,
import API answers 404/403 before the key check, favicon falls back to the
bundle's icon and is served only when it is an image, upload slot taken after
FormFile and released on a context that survives the request, quota release
semantics fixed, per-client SSE cap, list endpoints return 500 on a Redis
error instead of an empty page, /assets refuses directory listings, the
session store refuses Sentinel/address lists at boot, R2 sample placeholders
count as unset, ads/magnet responses carry a locked flag, /events accepts
last_id for manual reconnects, login rate limit runs before body decode.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GmkznW6GgQMoCPReH76A6
… guards

Scroll-to-message does one targeted load positioned around the target and
stops with a notice when the message is gone; the stored last-read id gets
one bounded jump. Manual SSE reconnects carry the last event id, a
channel-deleted event or a 404 shows the not-found page, anonymous viewers
share one user-info request, scheduled items no longer reuse real message
DOM ids, and a restored message is inserted for readers.

Raw HTML in messages loses style and foreign classes (a writer could overlay
the whole page); mailto:/tel: links keep their href; quotes with parentheses
and YouTube shorts/playlist URLs render correctly; "edited" no longer shows on
every message west of UTC; far-future scheduled posts show the time.

Composer: a rejected edit does not touch the feed, a chosen schedule time
does not leak into editing a live message (which corrupted the scheduled
list), pasted images respect the uploads switch, report reasons are capped.

Admin panel: magnet keys survive a save from the settings tab, saving is
blocked after a failed load (settings, magnet, features), a lock by the super
admin is shown, remove-then-re-add keeps the role, e-mails are validated,
reports and emojis update only after a successful save, the statistics chart
is chronological, storage shows "unlimited" for a zero quota. Super-admin:
delete confirmation and in-flight guard, slug normalisation and Hebrew
errors, zero-quota guard, request notes column. Header menu offers support
to every signed-in user and login returns to the current channel.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GmkznW6GgQMoCPReH76A6
…holders

The runtime image never contained assets/favicon.ico, so every tab had no
icon; sample.env's R2 placeholders enabled R2 against a non-existent host;
tracked Redis dumps are removed and ignored. DEPLOY.md documents the single
Redis address rule, WEBHOOK_ALLOW_PRIVATE, SECRET_KEY, per-migration budgets
and R2 verification; SET.md gets the real API paths and import contract,
the self-service channel flow, the magnet sandbox and settings that moved;
README describes the multi-channel flow; CHANGELOG lists the real routes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GmkznW6GgQMoCPReH76A6
…ues, magnet slot resilience

Two adversarial reviews went over the audit diff. What they refuted is
fixed here:

Backend: the scheduled-message whitelist dropped is_ads; channel deletion
now waits up to 15s for the scheduler's claim instead of failing at once;
settings validation covers only the entries a save changes, so a legacy
value stored before validation existed does not block unrelated saves
(one warning per key); ad-iframe-src accepts protocol-relative URLs as it
did before; super-admin demotion runs only when ADMIN_USERS is set; the
migration lock TTL outlives the migration budget; DEPLOY.md documents that
the proxy must pass the client address for the per-client limits.

Frontend: the embedded-quote tokenizer tries a strict pattern first so a
link on the same line is not swallowed; the emoji cache is updated in
place; picking a time on a live message warns instead of silently doing
nothing; the scheduled list is re-read after a commit; the logout button
shows only for a signed-in viewer; the magnet slot counts its grace from
the frame's load event, keeps the frame when hidden so a late ad reopens
the slot, and shims localStorage/sessionStorage/cookie inside the sandbox.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GmkznW6GgQMoCPReH76A6
…heduled re-post guard, stable ad slots

An independent background audit produced 248 verified findings. All of
them were cross-checked against this branch: 204 were already fixed, 9
are the documented deliberate gaps, and the 35 that remained are fixed
here, followed by two adversarial review rounds on these changes.

Backend: an anonymous support ticket is no longer indexed under the typed
e-mail when it is replied to; the ticket token is moved from ?token= into
the X-Ticket-Token header before the access log sees it; push subscribe
validates the token alphabet, is gated on the channel's notifications
feature and caps a channel at 50000 tokens; repeat reports answer success
without spending rate-limit quota and the compensating SRem runs on its
own context; editing a message writes only the edit fields so concurrent
view and reaction counters survive; channel deletion disables the channel
as a fence right after the role revoke, sweeps files in two passes and
snapshots the message index after the sweep; sseStopHub retires a hub on
delete and disable (disable publishes channel-disabled, the hub is
stopped 500 ms later so local viewers get the event), control events are
never replayed from catch-up, and a viewer admitted just before a delete
or disable is turned away after subscribing; the dispatcher remembers
posted scheduled entries for seven days so a stale tab cannot re-post
them; a per-hash lock and a conditional counter delete close the race
between a last-reference file delete and a same-hash upload; a new
api_secret_key must be at least 16 characters; privilegesMu spans the
whole users:list transaction, an unchanged profile no longer rewrites the
list on login, and contended writes retry with jitter; ownerEmail follows
the users editor; the SSE author mask follows live role changes; a
disconnect after deletion no longer recreates stats keys; approving a
stored channel request validates the name; the storage percentage is
rounded; favicon lookups check the slug; dump and appendonly files are
kept out of the image and the repository; CHANGELOG corrections.

Frontend: scrolling down pages from the newest loaded message in server
order and drops ids already shown; scheduled messages are saved, edited
and deleted against a fresh server list, identified by the displayed
entry's time and text, and a failed commit refreshes the feed; magnet ad
slot decisions are remembered per channel so slots do not move when older
pages load; a legacy same-line quote followed by a link keeps the link;
signing in from the files overlay returns to the channel; re-picking the
same logo fires change; max_file_size carries min and max in the form;
Hebrew messages for a short API key and a missing magnet key; anonymous
visitors on the disabled and not-found pages get a home button; a
channel-disabled event shows the disabled page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GmkznW6GgQMoCPReH76A6
Never show the operator's name or login to other users
Conflicts were in two places: the super-admin users list keeps the
existing-channel check and passes hideOperators=false, and getMessages
keeps the no-304-while-counting-views rule together with the "-op" ETag
marker.

Three follow-ups so the two changes hold together: recordAuthorID looks
the author up by the normalised address, which is how the live map and
the stored list are keyed on this branch; a scheduled entry an operator
saves is signed "ניהול" like a direct post, a carried-over author from
the stored list is re-labelled, and the dispatcher re-labels a legacy
entry before posting it; and an edit now writes the author fields when
the caller carries them, so a legacy operator post is stored anonymised
from its first edit while an update without an author still leaves the
stored one (and the counters) alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GmkznW6GgQMoCPReH76A6
סריקת מערכת הערוצים: אבטחה, אטומיות, פריסה ו-frontend
… הערוצים שלי

ערכת העיצוב עוברת לטוקנים של Nebular (themes.scss) — הכתובת היחידה לצבעים,
רדיוסים וצללים. default.css המובנה של Nebular הוסר (היה עותק כפול של כל ה-CSS),
Bootstrap מקומפל מתת-קבוצה של הרכיבים שבשימוש, ייבוא ה-CDN של bootstrap-icons
וגופן NotoColorEmoji בן 24MB נמחקו (אימוג'י מגופן המערכת). ה-CSS ירד מ-1.36MB
ל-0.98MB והתיקייה הנפרסת מ-27MB ל-3.4MB.

מסכי הפתיחה, ההתחברות ומנהל-העל נטענים עצלנית; Firebase נטען רק בערוץ שבו
ההתראות פעילות. פאנל ניהול הערוץ הופך לעמוד /channel/:slug/manage/:tab (עם
guard לרמת מנהל ומעלה). שירותים משותפים חדשים: ShareService (העתקה, וואטסאפ,
שיתוף מערכת), ConfirmService + דיאלוג אישור במקום window.confirm,
MyChannelsService.

בצד השרת: GET /api/my-channels מחזיר את כל הערוצים שבהם למשתמש יש תפקיד (שם,
לוגו, תפקיד, מושבת, משתתפים), ממוין בעלים קודם — הבסיס לעמוד "הערוצים שלי"
ולמעבר בין ערוצים; ודחיסת gzip לתשובות טקסט (SSE ומדיה מוחרגים לפי סוג תוכן).
אומת מקצה לקצה בשרת מקומי: main.js מ-534KB ל-144KB, זרם ה-SSE עובר לא דחוס.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8EB4vmzsK2EbLtytQGBd7
…ן רזה יותר

- השרת מזריק <title> ותגיות Open Graph לכל עמוד: בעמוד ערוץ ציבורי — שם,
  תיאור ולוגו הערוץ; בשאר — כרטיס המותג. ערוץ פרטי או מושבת לא חושף דבר.
- זרם האירועים מנותב סביב middleware הדחיסה: המעטפת של chi v1 לא תומכת
  ב-SetWriteDeadline, כך שמועד הכתיבה של getEvents מעולם לא נדרך.
- אייקון SVG, apple-touch-icon, מניפסט (עם סוג MIME רשום) וכרטיס og.jpg.
- רק ערכת המותג נפלטת ל-CSS (211KB פחות); טוקנים לטיפוגרפיה ותנועה;
  טוסטים נשארים 5 שניות ולא נערמים.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8EB4vmzsK2EbLtytQGBd7
עדיין לא מחוברים ל-app.config — יחוברו אחרי שכל המסכים יסיימו להשתנות,
כדי שאייקון חדש לא ייעלם בשקט באמצע העבודה.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8EB4vmzsK2EbLtytQGBd7
…הול, מנהל-על

- דף נחיתה ממותג שמסביר מה זה ערוץ; הכפתור עובר ישירות לכניסה עם גוגל.
- טופס פתיחת ערוץ במסך אחד: תצוגה חיה של הכתובת, הצעת כתובת פנויה,
  הסבר על מגבלת 5 הערוצים לפני שנתקלים בה, שגיאות בעברית.
- "הערוצים שלי": שם, לוגו, תפקיד, משתתפים, שיתוף, ניהול; ?new=1 פותח
  את הטופס מיד.
- עמוד הערוץ: כותרת עם מחליף ערוצים ותפריט שיתוף, מפרידי ימים, שלד
  טעינה, תפריט ⋮ להודעה עם דיאלוג אישור, תגובות בלחיצה, זמן ב-Intl.
- עמוד הניהול המנותב מחליף את חלון הפאנל (admin-panel נמחק): הסבר
  "מה זה?" בכל מדור, מחליף ערוצים, כל המדורים עוצבו מחדש.
- פאנל מנהל-על: טוקנים, דיאלוגי אישור, סרגל מדורים לטלפון, כותרת חלון.
- שילוב: שם המותג "הערוץ" בכל מקום; ה-guard מביא את /info במקביל
  לבדיקת ההתחברות ו-initChannel שומר אותו; תת-קבוצת אייקוני Eva
  במקום החבילה המלאה; הוסרו ng-bootstrap, popperjs ו-moment; כותרת
  נמוכה יותר בטלפון; סרגל הכתיבה נשבר לשורות במקום להיחתך.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8EB4vmzsK2EbLtytQGBd7
26 ממצאי הסקירה: בורר הזמן קרס בעריכת הודעה מתוזמנת (חותמת הזמן
מגיעה כמחרוזת), שליחה מתצוגה מקדימה השאירה תצוגה ריקה, ציטוט בזמן
עריכה שכתב את ההודעה הנערכת (ולכן שמירה של מתוזמנת נכשלה), תפריט ⋮
לא היה נגיש במקלדת, חזרה מגוגל (bfcache) השאירה ספינר, הודעת "מחפשים
כתובת פנויה" נתקעה, ניגודיות שבבי הכתובת, עמוד הניהול הרכיב מדורים
שאינם פתוחים לתפקיד, כרטיס ערוץ מושבת הפנה לכפתור שלא קיים, SET.md
הפנה לתוויות שהוחלפו, טוקן פנייה אנונימי פתח פניות של משתמש מחובר,
ועוד תיקוני ניסוח וגדלי מטרות.

בנוסף, באג שהסקירה לא תפסה ורק הדפדפן גילה: nbTooltip, nbPopover
ו-nbContextMenu מספקים כולם את אותו NbDynamicOverlayHandler על
האלמנט המארח, כך ששניים מהם על אלמנט אחד חולקים handler אחד והאחרון
שנבנה שולט בטריגר. tooltip ליד תפריט המשתמש, תפריט השיתוף ותפריט ⋮
השאיר את שלושתם מתים. ה-tooltips הוסרו מהאלמנטים האלה.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8EB4vmzsK2EbLtytQGBd7
…טופס פתיחה ועוד

סריקת Playwright מול שרת מקומי שחזרה 28 ליקויים בגרסה הקודמת. העיקריים:
- מעבר בין ערוצים מתוך האפליקציה (תפריט המשתמש, Back) השאיר את הפיד, זרם
  ה-SSE וכותרת הכרטיסייה של הערוץ הקודם — initChannel מריץ עכשיו change
  detection לפני שהוא טוען את הערוץ החדש, ומוותר אם התחלה חדשה עקפה אותו.
- תפריט השיתוף, תפריט המשתמש, כרטיס "על הערוץ" וחלון העזרה לעיצוב נגישים
  במקלדת: הפוקוס נכנס לתפריט, חצים/Home/End, Enter מפעיל, Escape ו-Tab
  סוגרים ומחזירים את הפוקוס, aria-expanded על הכפתורים ותפקידי menu/menuitem.
  תפריטי Nebular נבנים מחוץ ל-DOM של הרכיב, לכן המאזין ברמת המסמך.
- טופס הפתיחה: תווים לא חוקיים לא נשארים בשדה הכתובת, כתובת שנמחקה חוזרת
  להיגזר מהשם, Enter מחכה לתוצאת הבדיקה החיה, ו-?new=1 עוקב אחרי מה שמוצג.
- כרטיס "לא נמצא"/"מושבת": "לדף הבית" לא מרכיב את מעטפת הצ'אט ולא שולח
  בקשות לערוץ שאינו קיים; אין יותר שגיאות קונסולה על ads/settings.
- פיד: הודעה שנשלחה נגללת לתצוגה; סמן "לא נקרא" רק אחרי הודעה שבאמת נקראה;
  ראש הפיד מתחת לכותרת ולרצועה; ציטוט שומר כותרת קישור; קישור #הודעה גולל.
- guard מנהל-על מפנה מבקר לא מחובר לדף הכניסה; קופסת התמיכה מנקה התראות
  ובודקת פורמט אימייל; שדה רוחב הפרסומת נטען 300 ולא 0; נפחי אחסון משמאל
  לימין; טבלת הערוצים שומרת את האייקונים בשורה אחת ומצבי ריק לפי הסינון;
  טוסטים ברוחב טלפון; יעדי לחיצה בכותרת ≥40px; העתקה שנחסמה מציגה את הקישור.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8EB4vmzsK2EbLtytQGBd7
…וד ראש הפיד, פקיעת התחברות

אימות חוזר בדפדפן של 28 תיקוני הסריקה מצא ארבעה שנותרו חלקיים:
- טופס הפתיחה שפט "כתובת הוקלדה" לפי הקלט הגולמי, כך ששדה שהכיל רק עברית
  או סימני פיסוק (שמנוקה לריק) חסם את גזירת הכתובת מהשם, והשליחה נכשלה
  עם "לא הצלחנו להרכיב כתובת" בעוד השבב הבטיח שהשם קובע. הדגל נקבע עכשיו
  לפי הערך הנקי.
- ניווט פנימי של משתמש מחובר לערוץ חסר/מושבת עדיין הרכיב את מעטפת הצ'אט
  ושלח שמונה בקשות לערוץ שאינו קיים (user-info ממוזכר, לכן ה-guard חזר לפני
  שבדיקת /info הספיקה לסמן). ה-guard מחכה לבדיקה גם למחובר. בנוסף, הכרטיס
  עצמו דילג על טעינת ההתחברות ולכן הציג את ענף האנונימי ("לדף הבית" בלי
  "לערוצים שלי", "פנייה לתמיכה" והתנתקות) — ההתחברות נטענת גם על הכרטיס.
- ריפוד ראש הפיד ישב בתוך `@if meta.variable-exists(--input-height)` — בדיקת
  קומפילציה של משתנה Sass, לא של custom property — ולכן מעולם לא נפלט; מפריד
  היום הראשון וההודעה הראשונה נשארו מתחת לכותרת ולרצועה. הכלל נכתב ישירות.
- התחברות שפגה באמצע עריכה במנהל-על או בניהול הערוץ הציגה "השמירה לא
  הצליחה, נסו שוב" בלי רמז. interceptor חדש תופס 401 על נתיבי המפעיל, מוחק
  את המשתמש הממוזכר (אחרת דף ההתחברות החזיר את המפעיל מיד ללוח — לולאה),
  שומר returnUrl, מפנה להתחברות ומציג "ההתחברות פגה" — אחרי הניווט, כי
  Nebular מרכיב את ההודעות בתוך ה-nb-layout של הדף, שנהרס במעבר.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8EB4vmzsK2EbLtytQGBd7
…ul-w6lfwn)

שיפור מערכת הערוצים: נחיתה, פתיחת ערוץ ברגע, הערוצים שלי, מעבר בין ערוצים, ניהול ומהירות
The super-admin Magnet stats screen called Magnet's publisher-stats with the
public embed key in the query string. Magnet deleted that function because the
embed key is printed on every page, so anyone could read the earnings; the
screen has shown "site not found" since.

The global Magnet settings now hold a separate stats key (mgs_…), which the
server stores, never sends back to the browser (statsKeySet only), and passes
to Magnet's site-stats in the x-magnet-stats-key header.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U3kq3cjnn8zX8eYoBhabgK
- Every ad frame of a channel now gets the same random viewer id (seeded
  into the frame's in-memory localStorage as Magnet's nc_vid, stored per
  channel as magnet_vid:<slug>). Each frame used to mint its own, so Magnet
  saw every slot as a new viewer and its "prefer ads this reader has not
  seen" rotation never applied. Per channel, so different channel owners'
  snippets cannot link a reader across channels through it.
- When Magnet's widget fires magnet:empty and the frame shows nothing else,
  the slot collapses at once instead of showing an empty card for 5 s. It
  still reopens if an ad arrives later. Needs the new widget.js; with the
  current one nothing changes.
- The "by time" frequency hints said seconds of reading; the time is
  measured between the messages' post times.
- SET.md: per-slot fetch, viewer id, collapse and fallback described as the
  code does them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWVQoKvFhQ4TjBSB5j6kHT
- The frame flags "no ad" only when every Magnet slot in it is collapsed
  (data-nesher-state="empty"). magnet:empty also fires when a slot switches
  to the owner's fallback, and a fallback still getting its height (an image
  loading, content a script fills in) made the card collapse and jump back.
- The channel's viewer id is renewed every 24 hours, as long as Magnet
  remembers what a viewer saw, so a channel owner's snippet, which can read
  it, gets no lasting identifier.
- SET.md and the admin hints: min-time and per-seconds are measured from the
  message of the previous ad, by post times; the 20 s ceiling applies only
  when the frame has not loaded by then.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWVQoKvFhQ4TjBSB5j6kHT
פרסומות מגנט: מזהה גולש אחד לערוץ, וסלוט בלי פרסומת נסגר מיד
Merging to master deploys, an admin reply always shows "ניהול", and the build and test commands to run before a push, since the repo has no CI. Docs only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFqdt8Gn3SwF62Rz4obs3B
סטטיסטיקת מגנט: מפתח סטטיסטיקה נפרד במקום מפתח ההטמעה
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants