Skip to content

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

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

feat: עיצוב מחדש של מסך הגדרות הניהול + captain-definition#23
27180781 wants to merge 106 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
27180781 and others added 30 commits August 17, 2026 14:17
Two separate paths rendered nothing at all for a visitor without a
session, in both cases leaving the router with no active route and the
console with no error:

- AuthGuard ended in `return false`, which cancels the navigation
  outright rather than redirecting. It now returns a UrlTree to /login.
  The 401 branch is also widened: loadUserInfo() resolving with an empty
  body means "anonymous" just as much as a 401 does, and a non-401
  failure says nothing about the session so the route is allowed to
  render and surface its own error.
- ChannelComponent awaited loadUserInfo() unguarded on the slug-less
  route. The throw aborted ngOnInit with slugReady=false and
  noChannel=false, a combination the template renders as an empty page.

The attribution strip sat at the top of the scrolling column, above the
feed, and the feed opens scrolled to the newest message — so on a
channel with any history it was thousands of pixels off screen. It now
sticks to the bottom of the column, inside the gap the composer lift
already reserves. Measured visible in all four viewport/composer
combinations; previously off screen in all four.

Feed images now carry loading="lazy" and decoding="async", and media
elements preload metadata only. On a 25-image channel this takes the
first paint from 25 image requests to 6, with all 25 still arriving
across a full scroll. Both places the feed can emit an <img> are
covered: the custom embed extension and the plain markdown renderer.
No width/height pair is emitted for uploaded images because the backend
does not record their dimensions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yWNYaVjk7ozyguKLA8vvx
Feed images load lazily, so without a known aspect ratio the page grows
as each picture lands and shoves the reader's position around. Nothing
downstream could fix that because the size was never recorded: a file
record held only URL, filename, MIME type and byte size.

uploadFile now reads the pixel dimensions out of the image header and
stores them on FileMetadata, returning them in FileResponse. PNG, JPEG
and GIF go through image.DecodeConfig; WebP is parsed by hand across its
three encodings (VP8, VP8L, VP8X) rather than adding golang.org/x/image
for three integers. Only the header is read, never the pixel data.

The size is measured after TinyPNG compression, since that returns a
re-encoded file and the recorded size must be the one the browser
actually receives.

On the client the size rides in the embed token as
`[image-embedded#800x600](url)` and is rendered as width/height
attributes. Bootstrap's .img-fluid is `max-width: 100%; height: auto`,
so the browser uses the pair purely as an aspect ratio: the picture
still fits the 300px box, but the box reserves the right height before
the bytes arrive.

Every fallback is the previous behaviour rather than a guess. The size
group in the token is optional, so messages written before this change
tokenize exactly as they did and render with the old flat width="300".
An unreadable header, a non-image, a truncated upload or an implausible
canvas all yield no dimensions at all — a guessed ratio would letterbox
or distort the picture, which is worse than the reflow. The size is
carried in the token rather than the URL so the src stays byte-identical
to the path the server handed out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yWNYaVjk7ozyguKLA8vvx
index.html was served with no cache headers at all, so browsers and any
upstream proxy applied heuristic caching to the one file that must never
be stale. Every deploy renames the bundles by content hash, and this
document is the only thing that says which names are current. Held in a
cache past a deploy, it points at an /assets/main-<hash>.js that no
longer exists: the script 404s and app-root is left empty. That is a
white page with nothing in the console from the application, because the
application never started — and it appears for a visitor with a cold
cache while anyone holding a warm, self-consistent copy of the old
document and its old bundles keeps working.

index.html is now no-cache, must-revalidate. It is also rewritten per
deployment with the operator's title and analytics, which a shared cache
had no business storing either.

The content-hashed assets get a year and immutable, which is what makes
revalidating the small document on every visit cheap. Files under
/assets without a hash in the name are replaced in place rather than
renamed, so they revalidate after five minutes instead of being pinned.

Verified against a real production build: index.html and SPA routes
no-cache, main-<hash>.js and styles-<hash>.css immutable, favicon
max-age=300, and a request for a hashed bundle that does not exist
returns 404 — the failure this prevents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yWNYaVjk7ozyguKLA8vvx
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