- norda_events: kolumna event_date_end (NULLABLE, check constraint >= event_date)
- NordaEvent: property is_multi_day, date_range_display; is_past uwzględnia koniec
- Admin (new/edit): pole "Data zakończenia" w formularzu
- Calendar grid: wydarzenie wielodniowe wyświetla się na każdym dniu zakresu
- Upcoming/past filter: używa COALESCE(end, date) — 2-dniowe zostaje w Upcoming
do swojego ostatniego dnia
- event.html: "Termin" + zakres dla wielodniowych; ICS/Google end date z dateEnd
- Lekki markdown dla opisów: tylko **bold** → <strong> (audyt: tylko event #60)
Zero wpływu na 42 istniejące wydarzenia (NULL == stare zachowanie).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Logo: BIS Maszyny, Maszyny Norda, Sigma Budownictwo, Używane Linde, Wózki HDM (już na prod, brakowało w repo)
- .gitignore: .claude/worktrees/, scheduled_tasks.lock, .remember/, .superpowers/, runtime uploads/announcements/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ROADMAP: dodano funkcję #2 (e-deklaracja PZ) z analizą flow PDF + samodzielny podpis
- architecture/03,07,08,09,11 + flows/06: aktualizacja pod OVH VPS (IP, user maciejpi zamiast www-data, brak NPM dla prod)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Błąd: onclick="fn({{ name|tojson }})" — Jinja tojson generuje wartość
z cudzysłowami, które łamały atrybut HTML (zewnętrzne też cudzysłowy).
Efekt: klik w kwadrat nie robił nic.
Naprawa: przejście na data-* atrybuty + DOMContentLoaded event listener
z document.querySelectorAll. Zero escapingu, zero konfliktów.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Przed: w widoku rocznym /admin/fees kwadraciki miesięcy były tylko
dekoracyjne (span z tooltipem). Żeby wpisać płatność trzeba było
przełączyć widok na konkretny miesiąc przez dropdown i dopiero wtedy
pojawiał się przycisk „Opłać". Magdalena (kierownik biura) spędziła
8 minut próbując klikać w kwadraciki — nic się nie działo.
Teraz: każdy kwadrat miesiąca jest klikalny, otwiera okienko płatności
dla konkretnej firmy × miesiąca. Jeśli rekord MembershipFee nie istnieje
— backend sam go tworzy z wyliczoną stawką (200/300 zł wg zasad brand).
Zmiany:
- Nowy endpoint /admin/fees/ensure-and-mark-paid — tworzy rekord
jeśli brak, potem mark-paid. Odrzuca firmy-córki (parent_company_id)
z komunikatem „Płatność rejestruj przy firmie matce"
- openPaymentModalSmart() w JS — wybór między /mark-paid (istniejący fee)
a /ensure-and-mark-paid (nowy fee) na podstawie obecności feeId
- Hidden fields company_id, fee_year, fee_month w formularzu modala
- Modal pokazuje teraz osobno „Stawka" (disabled) i „Kwota wpłacona"
(editable) — jeden pole amount zmyliło Magdalenę
- Żółty info-box nad tabelą roczną: „Kliknij kwadrat miesiąca, aby
zarejestrować wpłatę"
- Hover: kwadrat się powiększa, pokazuje cień — afordancja kliknięcia
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Brakował wpis dla 13 kwietnia — 4 user-facing commity (mentions
autocomplete, email+highlight dla @, fix double-submit tematów).
Przenumerowanie: 14.04 z v1.67.0 na v1.68.0, nowe v1.67.0 dla 13.04.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rozszerzenie powiadomień o kolejne typy zdarzeń, z symetrycznymi togglami
e-mail i push w /konto/prywatnosc.
Migracje 103 + 104 — 6 nowych kolumn preferencji e-mail + NordaEvent.reminder_24h_sent_at.
Triggery:
- Forum odpowiedź → push do autora wątku (notify_push_forum_reply)
- Forum cytat (> **Imię** napisał(a):) → push + email do cytowanego
(notify_push/email_forum_quote)
- Admin publikuje aktualność → broadcast push (ON) + email (OFF)
do aktywnych członków (notify_push/email_announcements)
- Board: utworzenie / publikacja programu / publikacja protokołu
→ broadcast push + opt-in email (notify_push/email_board_meetings)
- Nowe wydarzenie w kalendarzu → broadcast push + email (oba ON)
(notify_push/email_event_invites)
- Cron scripts/event_reminders_cron.py co godzinę — wydarzenia za 23-25h,
dla zapisanych (EventAttendee.status != 'declined') push + email,
znacznik NordaEvent.reminder_24h_sent_at żeby nie dublować.
Email defaults dobrane, by nie zalać inbox: broadcast OFF (announcements,
board, forum_reply), personalne/actionable ON (forum_quote, event_invites,
event_reminders).
Wszystkie nowe e-maile mają jednym-kliknięciem unsubscribe (RFC 8058
+ link w stopce) — unsubscribe_tokens.py rozszerzony o nowe typy.
Cron entry do dodania na prod (osobny krok, bo to edycja crontaba):
0 * * * * cd /var/www/nordabiznes && venv/bin/python3 scripts/event_reminders_cron.py
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Link z listy tematów (forum/index, dashboard, strona główna) prowadzi
teraz do kotwicy #reply-latest — wstawionej w topic.html tuż przed
ostatnim widocznym replyem. scroll-margin-top:90px żeby sticky nav
nie zasłaniał. Brak zmian w backendzie (anchor HTML, zero round-trip).
Admin panele (forum_reports, forum_analytics, forum.html, forum_deleted)
zostawiam — tam moderator ogląda temat od góry.
Topic bez replies (sam początkowy post) zachowuje się bez zmian
(anchor nie istnieje → scroll na górę).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Każdy e-mail powiadomieniowy ma teraz:
(1) link w stopce "Wyłącz ten typ powiadomień jednym kliknięciem"
(2) nagłówki List-Unsubscribe + List-Unsubscribe-Post dla klientów
pocztowych (Gmail/Apple Mail pokażą natywny przycisk Unsubscribe)
Implementacja:
- utils/unsubscribe_tokens.py: signed token (itsdangerous, SECRET_KEY)
niosący user_id + notification_type, bez wygasania
- blueprints/unsubscribe: GET /unsubscribe?t=TOKEN → strona potwierdzenia,
POST /unsubscribe → faktyczne wyłączenie flagi notify_email_<type>
- email_service.send_email() dostał parametr notification_type. Jeśli
przekazany razem z user_id, footer + headery są doklejane
- Aktualizowane wywołania: message_notification (messages),
classified_question/answer (B2B Q&A), classified_expiry (skrypt cron)
Prefetch safety: GET pokazuje stronę z przyciskiem "Tak, wyłącz",
wyłączenie następuje po POST. RFC 8058 One-Click (POST bez formularza
z Content-Type application/x-www-form-urlencoded + body
"List-Unsubscribe=One-Click") obsługuje klientów pocztowych.
D.2/D.3 dorzucą kolejne notification_type (forum, broadcast, events).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Symetria z push — panel /konto/prywatnosc rozszerzony o 3 dodatkowe
toggle w karcie "Powiadomienia e-mail":
- Pytanie pod moim ogłoszeniem B2B (notify_email_classified_question)
- Odpowiedź pod moim pytaniem B2B (notify_email_classified_answer)
- Ogłoszenie wygasa za 3 dni (notify_email_classified_expiry)
Migracja 102 dodaje kolumny (default TRUE — nie zmienia zachowania
istniejących userów). Endpointy ask_question / answer_question teraz
czytają dedykowaną flagę zamiast notify_email_messages (która zostaje
tylko dla wiadomości prywatnych). Skrypt classified_expiry_notifier.py
pomija userów z wyłączonym notify_email_classified_expiry.
W kolejnych sub-fazach D.2/D.3 symetrycznie dojdą triggery e-mail +
toggle dla forum/broadcast/wydarzeń — z defaults dobranymi tak, by
nie zalać inbox użytkowników (broadcast OFF, personalne ON).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migracja 101 dodaje 8 nowych kolumn notify_push_* na users (wszystkie
default TRUE). Panel preferencji rozszerzony o kartę "Powiadomienia
push (na urządzeniu)" z 3 podsekcjami (interakcje dot. mnie, aktualności
Izby, wydarzenia) — 9 przełączników. "Nowa wiadomość prywatna" świadomie
jest w obu kartach (e-mail + push) — userzy mogą niezależnie wybrać
oba kanały.
Triggery B2B:
- zainteresowanie ogłoszeniem (ClassifiedInterest) → push do autora
z notify_push_classified_interest
- pytanie do ogłoszenia (ClassifiedQuestion) → push do autora z
notify_push_classified_question
Fazy D.2 (forum + broadcast) i D.3 (wydarzenia + cron) w kolejnych PR.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Odróżnia push od portalowych powiadomień w navbarze. Po pierwszym
udanym włączeniu pokazuje komunikat że subskrypcja działa tylko na
tym urządzeniu i zachęca do kliknięcia również na innych telefonach
i komputerach. Tooltipy zaktualizowane pod kątem "na tym urządzeniu".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
B2B ogłoszenia mogły zostać stworzone 3x (user 81 Bormax 14.04.2026
w ciągu 2 sekund) — brak dedup window server-side i disable submit
button. Rozszerzam zabezpieczenie także na announcements i board
meeting form.
- classifieds POST /nowe: odrzuć duplikat z ostatnich 60s (ten sam
author+company+title) → redirect do istniejącego z flash info
- classifieds new.html: disable submitBtn + "Wysyłanie..." po
walidacji; ponowne kliknięcie blokowane event.preventDefault
- announcements_form.html + board/meeting_form.html: jednolity
handler disable wszystkich button[type="submit"] po pierwszym
submit
Forum topic/reply już miały analogiczne zabezpieczenie (bez zmian).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Google Maps URLs can be 800+ chars of tracking data that poisoned the
forum UI. Extract the place name from /maps/place/NAME/ (or fall back
to coordinates) and render as '📍 Name'. Full URL remains in the href.
Two secondary fixes:
- Edit/quote modals were reading .innerText of the rendered reply,
which baked the current render (including any stale/broken HTML from
older bad renders) back into the textarea. Switched to emitting the
raw DB content via {{ content|tojson }} so what you edit is what you
wrote.
- @mention regex was matching '@54.1234' inside Maps URLs and similar.
Tightened to require a letter start and non-slash/non-word lookbehind
so coords and email-style strings pass through untouched.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Forum reply, @mention and classifieds notification emails used
`background: linear-gradient(...)` for the primary CTA button. Outlook
desktop strips background-image gradients, leaving the white-on-white
text effectively invisible. Added background-color as the first
declaration so Outlook keeps a solid dark-blue button while modern
clients still render the gradient.
Reported by Maciej Koenig: forum reply email appeared to lack the
'Zobacz odpowiedź' link.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SQLAlchemy ORM tries to UPDATE classified_reads.classified_id = NULL
before deleting the classifieds row, even though the FK has ON DELETE
CASCADE at DB level. The NOT NULL constraint on classified_id then
raises IntegrityError. Same pattern as the forum_reply_reads fix from
2026-02. Manually delete reads, interests, questions, attachments
before db.delete(classified).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The :invalid CSS without scoping was making the entire form-container
draw a red border (the form is :invalid as long as any inner field is).
Removed it. Replaced with positive feedback: a green ✓ appears next to
the label of each required field as soon as it is filled. Tracks title,
category, listing_type radios and Quill description (new.html) plus
title and description (edit.html). Initial pass at load sets the check
on values restored after a POST validation error.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
document.querySelector('form') was matching the company switcher form
in the navbar, not the classifieds form. Quill content was therefore
never synced into the hidden description textarea, server saw it empty
and rejected the submit with a misleading 'fill all fields' error.
Added id='classifiedForm' to both new.html and edit.html and switched
selectors to getElementById.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously when server validation failed (e.g. missing required field),
the whole form re-rendered with all values cleared — user had to retype
everything. Also Quill empty-content showed an alert dialog.
Now:
- Server-side: form_data + missing_fields passed to template; values
re-populate inputs, missing fields get .field-error class (red border)
- Quill empty: red border on the editor container instead of alert,
cleared as soon as user starts typing
- Other required fields (radio, select, title): same .field-error
treatment plus :invalid CSS for live HTML5 feedback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two user-reported regressions:
1. B2B classifieds "Dodaj ogłoszenie" button silently no-op'd. Hidden
Quill description textarea had `required` attribute — browser blocked
submit but cannot show validation UI on display:none fields. Removed
`required`, added empty-content guard in submit handler with explicit
alert.
2. Forum auto-linker truncated Google Maps URLs containing two
underscores (e.g. forestry_office...g_ep=). Italic regex `_text_`
matched across the URL, splitting it with <em> tags. Italic/bold
underscore forms now require non-word boundary so URLs and identifiers
like my_var_name pass through untouched.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Meta deprecates page_impressions, post_impressions, page_video_views et al.
on 2026-06-30. Replaced by *_media_view family. Both old and new metrics
are requested during the transition window so historical data and fresh
data coexist without UI gaps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove _send_registration_notification call on registration
- Remove first-activation admin notification on password reset
- Change MAIL_BCC default from maciej.pienczyn@inpi.pl to empty
Admin can still set MAIL_BCC via env if blanket BCC is desired.
All new member and password-reset info is visible in /admin panel.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- parse_mentions_and_notify now sends email to mentioned user
(separate from forum subscription emails — fires on every mention)
- parse_forum_markdown accepts current_user_name; mentions matching
the viewer get extra .forum-mention-self class
- topic.html passes current_user.name to filter; .forum-mention-self
styled with amber background + bold + ring
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Backend: reject identical title+content from same author within 60s
(mirrors existing protection on forum_reply)
- Frontend: disable submit button + 'Wysyłanie…' label on first click
Daniel Kochański accidentally created 7 identical 'Local content w praktyce'
topics within 5 seconds. Soft-deleted IDs 25-30 on prod, kept 24.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Deduplicate by sender+content match regardless of _optimistic flag,
preventing double display when SSE event arrives before POST response.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Admin fees yearly view now shows all active companies (including child brands)
- Child brand rows are indented with striped month cells and "firma córka" badge
- Parent companies show expandable brand list, Stawka column with 200/300 zł logic
- Expected fee per month computed from number of active child brands
- Rate change month shown when brand joins mid-year (e.g. "I-III: 200 zł / od IV: 300 zł")
- Sorting groups children directly under their parent
- Reminder logic skipped for child companies
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Legacy private_messages and group_messages are no longer used.
Badge now only counts from conv_messages table.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Shows expected fee per company (200 zł for 1 brand, 300 zł for 2+)
- Child companies shown with striped "nie dotyczy" tiles
- Rate change month displayed (e.g., "I-III: 200 zł, od IV: 300 zł")
- Expandable brand list under parent company name
- Children grouped after their parent in the table
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add sort keys and data-sort-value attributes to 'Upr. firmowe' and 'Rola' columns
- Add filter tabs for MANAGER, OFFICE_MANAGER, company-role NONE and MANAGER
- Add data-company-role attribute to user rows for JS filtering
- Grant OFFICE_MANAGER access to admin_users, assign-company, reset-password, change-role, get-roles endpoints
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All "Powrót do wiadomości" links in compose, view, sent, and group_compose
templates now point to messages.conversations_page instead of legacy
messages_inbox.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- "Profil firmy" button becomes a dropdown listing all companies
- Subtitle under name shows all companies separated by dots
- Single-company users see no changes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
URLs in message content were rendered as plain text. Added linkifyContent()
that converts URLs to clickable links while preserving existing <a> tags.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The badge endpoint api_unread_count only counted legacy private_messages
and group_messages. Now also counts unread conv_messages from the new
conversations system, fixing phantom unread counts for users.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace company_id from current_user with active company from session in
the colleagues API endpoint, and autofill guest org from active_company.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>