Repository navigation
Conversation
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
- 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
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
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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cRP8dF3PN7CxSHKgQdUdv
סטטיסטיקת מגנט: מפתח סטטיסטיקה נפרד במקום מפתח ההטמעה
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
regex-replace(שדות pattern/replace נפרדים), הדבקת JSON של Service Account שמפצל אוטומטית לכל שדותfcm_json_*, ומקטע "הגדרות מתקדמות" מתקפל בתחתית שמאפשר עדיין הוספת key/value חופשי (גם מציג הגדרות לא מוכרות שכבר קיימות).[{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
1/לא קיים).regex-replaceולוודא שהם נשמרים בפורמטpattern#replaceופועלים בפרסום הודעות.fcm_json_*מתמלאים.🤖 Generated with Claude Code