From 4f209aac8e59d6b5c880ad5ccbf7cc7308c83303 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 3 Jul 2026 19:38:20 +0000 Subject: [PATCH] g --- config/__pycache__/__init__.cpython-311.pyc | Bin 159 -> 159 bytes config/__pycache__/settings.cpython-311.pyc | Bin 5552 -> 5552 bytes config/__pycache__/urls.cpython-311.pyc | Bin 1557 -> 1557 bytes config/__pycache__/wsgi.cpython-311.pyc | Bin 679 -> 679 bytes core/__pycache__/__init__.cpython-311.pyc | Bin 157 -> 157 bytes core/__pycache__/admin.cpython-311.pyc | Bin 212 -> 1731 bytes core/__pycache__/apps.cpython-311.pyc | Bin 524 -> 524 bytes .../context_processors.cpython-311.pyc | Bin 763 -> 763 bytes core/__pycache__/forms.cpython-311.pyc | Bin 0 -> 4137 bytes core/__pycache__/models.cpython-311.pyc | Bin 209 -> 9729 bytes core/__pycache__/urls.cpython-311.pyc | Bin 347 -> 783 bytes core/__pycache__/views.cpython-311.pyc | Bin 1364 -> 15118 bytes core/admin.py | 25 +- core/forms.py | 46 +++ core/migrations/0001_initial.py | 56 +++ core/migrations/0002_webhookevent.py | 38 ++ .../__pycache__/0001_initial.cpython-311.pyc | Bin 0 -> 4478 bytes .../0002_webhookevent.cpython-311.pyc | Bin 0 -> 2426 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 168 -> 168 bytes core/models.py | 152 +++++++- core/templates/base.html | 57 ++- core/templates/core/call_detail.html | 83 +++++ core/templates/core/call_form.html | 94 +++++ core/templates/core/call_list.html | 64 ++++ core/templates/core/index.html | 237 +++++------- core/templates/core/webhook_list.html | 94 +++++ core/urls.py | 9 +- core/views.py | 288 ++++++++++++++- static/css/custom.css | 336 ++++++++++++++++- staticfiles/css/custom.css | 343 +++++++++++++++++- 30 files changed, 1734 insertions(+), 188 deletions(-) create mode 100644 core/__pycache__/forms.cpython-311.pyc create mode 100644 core/forms.py create mode 100644 core/migrations/0001_initial.py create mode 100644 core/migrations/0002_webhookevent.py create mode 100644 core/migrations/__pycache__/0001_initial.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0002_webhookevent.cpython-311.pyc create mode 100644 core/templates/core/call_detail.html create mode 100644 core/templates/core/call_form.html create mode 100644 core/templates/core/call_list.html create mode 100644 core/templates/core/webhook_list.html diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 896bb4f1b8949c96d2f1bf743293342b70a9d0e4..1c700f1cbc511ec5d6c3b1e1cf0ce5cf9fee5e92 100644 GIT binary patch delta 20 acmbQwIG>ScIWI340}yQE^2nOVGZ_Fb)&#l$ delta 20 acmbQwIG>ScIWI340}#~ttjL_mGZ_Fdwgn>q diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index d79d6a79890a58443ee5f65d8d558823a19e9aed..c73f8849691d68218e22355ab393ad5421f1f687 100644 GIT binary patch delta 21 bcmdm>y+NC2IWI340}yQE^2plAvqTgCJu3xv delta 21 bcmdm>y+NC2IWI340}#~ttjOHRvqTgCKXe7( diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 8cf22af743397374cabf5fa358c4f52631471db9..02d2eb5d7a47b5db93687616b1698ee3e79071a0 100644 GIT binary patch delta 21 bcmbQrGnI#DIWI340}yQE^2plA!^s8!GZX|^ delta 21 bcmbQrGnI#DIWI340}#~ttjOHR!^s8!HC+V3 diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index a1b4aa75cef92df0f6f3c8f77e2be44ec1b36534..720e0185ec64cb067bd0bd0ffe5c5add506fd1f7 100644 GIT binary patch delta 21 bcmZ3^x}23~IWI340}yQE^2plAGn)wjHTnen delta 21 bcmZ3^x}23~IWI340}#~ttjOHRGn)wjI70u$ogOPQ7iEo_gwgb~ZF5AjbC3Z{9q=k9oiGZ|!!2z?hl5 zvE8W>@)u6V!#Ng?{siG6;e=B~BI;90xWy~ViYk63vVEHpiwL?6-Mk0->h{llYujzGsu#z4uzTO zLM_wWG+C!eD#=CWS$YD^8r_s1hgkD!rXCbI2Z3n>L6q>W5+JvN;9i$0tOjRFW_iG6 zmMR7b6thjKa-mH-6HJF&!KM_7XJ!)8=Si%3$BN&{5yT|V+hL-``$bz;(w?q^t^g!V zb`_w1avjX*{>|a^M*q9v?0SE7c=3b&Exh!?_@h&e8x!;BD_H)o#`!hw@EWg6x@mD2 zq(&h%L29A2S@G+<4egyUQ!mq`>@aLps*@ebq2}r1od^WdD9C#$^jiE3+YvJrNN$>3 zD7hna4<~L(fIz0k1s9>nG65!9gaT}ePTTr*NZvI5_NU*%V&4YvoMKTgqv8#OHvvpt zv-`nD!h5C~7u{{1XQr>AZ7WRT{3O%b7PLs{%(U)xF~yW=7Gi)5_zUyMVo>xc$oFl7$!l;IKazf z`xVJ{1Ifnvt0RkCimd*v;q2A^>VZ4|=W)0ox-MfR=z8}o459)V@ z?$qv$!Qw}QnU4qcPewI+%ISZLSD66DhI-5d-)3%gcwj@C*UHES|M5_U{Fa^1hz{ literal 212 zcmZ3^%ge<81a&?uGVOu%V-N=hn4pZ$LO{lJh7^Vr#vF!R#wbQch7_h?22JLdAO)I? zw^$QXax?S%G?{MkrDP@MrRVD<=jW9aWhNCd0~M@f_zY6_OIJT5KQ~psG^sSNq*On( zA~m_RB)>?%JijQrxF9h(RX;huC{-V9lwLvQFAkgB{FKt1RJ$S$pl(JWE|vunAD9^# P8E-HsT)>8k*nlbkB)c`? diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 2fa4a49f9779402d93d4c58688d65ede2fff84d5..6ef71992c6eed0a1a41bd791b89f2c2b4ac5fad6 100644 GIT binary patch delta 20 acmeBS>0#kn&dbZi00i5(JhB$@FaZE84+I$i delta 20 acmeBS>0#kn&dbZi00ebDD>4`IFaZE9?*xJX diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 75bf2234fb21a6b62efc5cec11af9512dd0c9616..b3ec9ef1de1ba44c20a2aa977bb54d2c3974cef2 100644 GIT binary patch delta 21 bcmey(`kR$!IWI340}yQE^2plA^PUL+L&634 delta 21 bcmey(`kR$!IWI340}xbw%g@}%^PUL+NjnD4 diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4ceba4bfd0232a64cb891f5db4d012b3733f2de GIT binary patch literal 4137 zcma)8U2GHC6~5!Y*p5xekAoe@;6Orf66X)xLYG~jAtAps1n5H+G@W>EoC#x(xibR^ z0*Qx}ns#XwsZ?uesf4PQ-3lt@F%NynBQKS%(MUuit+Z;Ncv%esRTWP?cLoQ7y%pEv zGw0s>opaAUbMHCln{YUYpuGx?FT3gy`Zw!Tn)fa9>MSr{ArXn3jPl$($05-rx@A}1 zJ@4j_3!Ouv=L;lyF}Lig2tRTo^cen5oA-*oizrq1f^}1z>0MMvUVXtiy}+y_E82)4 z%UK8R(2A~C$D#jgumGcZPDJxAdqoabbUkvxD)4vOyn78Ta-#bS|GZ}nt#I>RSkq&M zchrx;s=Jn`dn@_sKcM8@0@yw8$-#F23@Rj8#)YtNxq?NaTl7fW6YnE1|1sNw-TuId zzA9W@O`PAx`74s3FHjQ~v~htQT)h|)!w+0FR^IagvO(PYBgn=NkWFIqk04uYqshQi+ylG(r=n}U6B46tO==vS)Y|9T+sM}kjJLC zxTF-|h4X?W!xY?AUR89G#b)EGhGkyRMM>dRP0)1JY?PIplvR15C{>1n8W9R=mPkd- z^h-qLMZBPyK_-A5KA;IJ{3;fRYK9jjSt{fzvJE(2l$F&yvqoDjVl${@vwBg`B&A@s zouHs79pI3;M6LQ$}5YS=u)cLF3k80Q%Redkjl)6d(Nf#=O2&*v3U zmoe}mp1-aOGP@=`Pgv44y)cPo)3+#LSyT!0j~TdxSG0*jQHN7M%5IUv``NHSuwd4o z*YgXQIKy=}B{2sl>~H852%|UYfhF+mfWDv?G<^WC;H<7GWZHTd&)G(O^bi4f4&Hnjpw zEQrF9^};&7U)VT9$F9>}!k$*O+^QO_s#VW0Arx*aeFnyVw%JIhR63}!C$5*{x)Il{ zFvILY;e9lkDtV1)Y9p~#M+e7gbfO%cFrpJy14HZu18G}tsmo~V-S~9tFdd$tZIk7; zNuzDjYGjBe6h1^dGY?0N&dlc3r(^WwMcR3(+qL<nTNySPy)z2pu{{W8>x6xDgw-;tbIa9-;BRQpSk)ZOm+C=+Oxp zpDf2GjrgRMV2J(yPsagU1C0KW`wFT~?Y;R&F!dPzPFvoYxy?!Fw(Bq4@1bVBjh1&R#fR)l zR<+$)&Z4Woz2nw)$}fa&yO%39G16FwecRrZhP&%g;u5(6C%W!n<=n=iUb$pFP-)+y==uhs4EMHW$-8-+nmgqSNa`(Oz>)!XI=x2e} zY5a~)KEMP%0u!u>K~SL7pTD)2MYeQy31_dRB^E<(g`5${{X~&()!RK4%aA=xZ&rpx zW1=@)@{*lM?Hp;RzEs%ss(KL<7R#jqdZlO^-lwP%&J)X}`U8PB4QD z0#qlCp#k1ucwY5^x+H6N$tbX78py5I-Q`)u@Gh8Be*p5|yJ*`V{hjwWb@%EFf3(y| z{i%nCfjG~0BzFJv=aKGbk?wLNWkgaI@&uM3`ZBsU`rW{Z&8~m+{nKlVe_kHAVhmiN zu_BH2vS*>VHu+sc48nEa-@Q*l<%W#Wkg*UK?%huGtOvK#!%wal=~K_sBhS(!Tj!q6 zmD96EdY1O^?74W=VV?KkXFa&wlQVj9Yoqt3jnMwm&2p%lhPt==hu1v!>epus|3Smw zyM3h3?w&LJ?S{W=J3VA~rwxCH;qRgLV+V+okDOrs`57ahFfzsnbC(%@>&`5O8kJ9R zWFAIU77|W7PGu>PFv7g8{t5_qu=2B9?OupS)#N1`d;M1O5kfHzCCU literal 0 HcmV?d00001 diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index a251b5fb7c2b603c587f8162d643eb10596d2858..3f3803244efff41c4aebd92955273c0b06d45b90 100644 GIT binary patch literal 9729 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 P;@ppj|LHB2RF3}xlp}(@ literal 209 zcmZ3^%ge<81a&?uGA)7hV-N=hn4pZ$LO{lJh7^Vr#vF!R#wbQch7_h?22JLdAO)I? zx7c#?Q&Mw^{WO_wai(M?=B4NBr6d(G10`27do?nz*T#%TYs-K)+l&TLgMz5gq7l#dyU7C|>SHuC-&IrWCvOwYkGb1D84F;JD K*iaE0Pz3-{*EE9w diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index f705988a223abc7b5af333902925bc196186bbf5..653b1c831553efd6d813e7c9f4cbc88851100eec 100644 GIT binary patch literal 783 zcmb7Bzi-n(6uyg}&vt%b(k=)BnX}XjQUX|=AVNZ7fyoS&$(@q4K07+QjR;G}j@ih_ z1VQk>U?UHaCnTnBk-Bx_d`Xc&S$OwD`o8bI@9y1wZZxV$#=iahc!d%ARf3yPiUe0b zR8H zYVK4lrqSw7rN)5PcPb5L(px(fn;G=>zsit!7Hz&loxRKQN*!Fl#7{<-vVGJcMQz{* z!O=i4KVe%yS>lg_!cs~wj*|jL5od)xWp77ObaFIgArs?4QNJNn5)$UtKlC}9k+M8? zgdxdUbpt{+b%b6L_J(lBVe)u#a_9(Bvewp6mX*ITYyQF8BOPH${$;0H)abDv4x@up z5yYZ=?m|BsvuUjCm76+Q-ANTG$D$_lW%~&eVf+PM$vSjhA4GyVrP0CU-L>iRh@J-Q zQ0z;we5Lri+}0Y#c!gT&-}5JWlm5?K6Qo<`PcQZ^UN1-r?F`yEv{ztC?#}$t$0sQ` z88|sO8?H5fzr3G9H-l~tUB#u=-NlO~Pr=Q=&B5Iy-kS%@2PyP2=;hGcCbkw)`tVr_ U{S5j!^w$l;uAJA(A)2zM-xXuemH+?% literal 347 zcmZ3^%ge<81a&?uGP8j6V-N=hSfPy1HbBO7h7^Vr#vF!R#wbQch7_h0<{YM6<|t-H z1}277#uS!R<`mX7Y|EG#7*+!@1f(-Yv81pEGiY+W1PN#|-eM_8EXjDu2<2qt=ce9b zC<567lFLiXP4&}cy~Ul9m6(^FuUA@>Q(Ocx>=tWTW@>pcSm7;hpfJ#slGLKS;*|`a zfzk}Wbo4<+>6a#z=9QG{r&gpUmzLxg>6hmhWfvDDCa3Br=NAE`f#&HIRQ}?y$<0qG z%}KQ@;sa_1Ii}bPNPJ*sWMsU-Aael~Jzx;OfQmk_F|cwrxO510gv?;Oz#@N*W8V3NU&sfd? diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2f0989c7137f7930a63ea76fcd63516c4974e834..0d3d5937b1919fe42be63e438c5f8cd14be07f97 100644 GIT binary patch literal 15118 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 Xr9HlS=yH2}4P;gNa)91vejfh;`i@)& diff --git a/core/admin.py b/core/admin.py index 8c38f3f..95f9704 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,26 @@ from django.contrib import admin -# Register your models here. +from .models import PilotCall, WebhookEvent + + +@admin.register(PilotCall) +class PilotCallAdmin(admin.ModelAdmin): + list_display = ('client_name', 'phone', 'call_source', 'ytel_audio_status', 'logics_api_status', 'created_at') + list_filter = ('call_source', 'ytel_audio_status', 'logics_api_status', 'levy', 'garnishment') + search_fields = ('client_name', 'phone', 'email', 'transcript', 'agent_notes') + readonly_fields = ('created_at', 'updated_at') + + + +@admin.register(WebhookEvent) +class WebhookEventAdmin(admin.ModelAdmin): + list_display = ('provider', 'display_event_type', 'external_id', 'delivery_id', 'source_ip', 'processed', 'received_at') + list_filter = ('provider', 'event_type', 'processed', 'received_at') + search_fields = ('provider', 'event_type', 'external_id', 'delivery_id', 'raw_body', 'notes') + readonly_fields = ('provider', 'event_type', 'external_id', 'delivery_id', 'source_ip', 'content_type', 'headers', 'query_params', 'payload', 'raw_body', 'received_at') + list_editable = ('processed',) + fieldsets = ( + ('Routing summary', {'fields': ('provider', 'event_type', 'external_id', 'delivery_id', 'source_ip', 'content_type', 'received_at')}), + ('Payload', {'fields': ('query_params', 'headers', 'payload', 'raw_body')}), + ('Review', {'fields': ('processed', 'notes')}), + ) diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..8201e6f --- /dev/null +++ b/core/forms.py @@ -0,0 +1,46 @@ +from django import forms + +from .models import PilotCall + + +class PilotCallForm(forms.ModelForm): + class Meta: + model = PilotCall + fields = [ + 'client_name', 'phone', 'email', 'call_source', 'ytel_audio_status', 'logics_api_status', + 'transcript', 'irs_debt', 'state_debt', 'tax_years', 'filing_status', 'employment_type', + 'occupation', 'monthly_gross_income', 'spouse_income', 'mortgage', 'rent', 'utilities', + 'car_payment', 'insurance', 'student_loans', 'credit_cards', 'assets', 'business_expenses', + 'irs_payment_plan', 'levy', 'garnishment', 'best_callback_time', 'agent_notes', + ] + widgets = { + 'client_name': forms.TextInput(attrs={'placeholder': 'Maria Johnson'}), + 'phone': forms.TextInput(attrs={'placeholder': '(555) 013-8842'}), + 'email': forms.EmailInput(attrs={'placeholder': 'client@example.com'}), + 'transcript': forms.Textarea(attrs={'rows': 8, 'placeholder': 'Paste the latest transcript segment from the call…'}), + 'assets': forms.Textarea(attrs={'rows': 3, 'placeholder': 'Home equity, vehicles, business assets…'}), + 'agent_notes': forms.Textarea(attrs={'rows': 4, 'placeholder': 'Summary, objections, follow-up plan…'}), + 'irs_debt': forms.NumberInput(attrs={'placeholder': '24000'}), + 'state_debt': forms.NumberInput(attrs={'placeholder': '5000'}), + 'monthly_gross_income': forms.NumberInput(attrs={'placeholder': '7200'}), + 'spouse_income': forms.NumberInput(attrs={'placeholder': '3800'}), + 'mortgage': forms.NumberInput(attrs={'placeholder': '3500'}), + 'rent': forms.NumberInput(attrs={'placeholder': '0'}), + 'utilities': forms.NumberInput(attrs={'placeholder': '450'}), + 'car_payment': forms.NumberInput(attrs={'placeholder': '620'}), + 'insurance': forms.NumberInput(attrs={'placeholder': '310'}), + 'student_loans': forms.NumberInput(attrs={'placeholder': '250'}), + 'credit_cards': forms.NumberInput(attrs={'placeholder': '900'}), + 'business_expenses': forms.NumberInput(attrs={'placeholder': '1800'}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field in self.fields.values(): + base_class = 'form-check-input' if isinstance(field.widget, forms.CheckboxInput) else 'form-control' + if isinstance(field.widget, forms.Select): + base_class = 'form-select' + field.widget.attrs['class'] = f"{field.widget.attrs.get('class', '')} {base_class}".strip() + self.fields['client_name'].required = True + self.fields['phone'].required = True + self.fields['transcript'].required = True diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..dc46608 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,56 @@ +# Generated by Django 5.2.7 on 2026-07-03 19:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='PilotCall', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('client_name', models.CharField(max_length=140)), + ('phone', models.CharField(max_length=40)), + ('email', models.EmailField(blank=True, max_length=254)), + ('call_source', models.CharField(choices=[('ytel', 'Ytel'), ('ringcentral', 'RingCentral fallback'), ('manual', 'Manual/recording test')], default='ytel', max_length=24)), + ('ytel_audio_status', models.CharField(choices=[('unconfirmed', 'Not confirmed'), ('available', 'Available'), ('blocked', 'Blocked / not exposed'), ('partner', 'Needs enterprise/partner access')], default='unconfirmed', max_length=24)), + ('logics_api_status', models.CharField(choices=[('unconfirmed', 'Not confirmed'), ('available', 'Available'), ('blocked', 'Blocked / not exposed'), ('partner', 'Needs enterprise/partner access')], default='unconfirmed', max_length=24)), + ('transcript', models.TextField(help_text='Paste a live transcript segment or recorded-call transcript for the pilot.')), + ('irs_debt', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('state_debt', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('tax_years', models.CharField(blank=True, max_length=120)), + ('filing_status', models.CharField(blank=True, max_length=80)), + ('employment_type', models.CharField(blank=True, max_length=120)), + ('occupation', models.CharField(blank=True, max_length=120)), + ('monthly_gross_income', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('spouse_income', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('mortgage', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('rent', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('utilities', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('car_payment', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('insurance', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('student_loans', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('credit_cards', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('assets', models.TextField(blank=True)), + ('business_expenses', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('irs_payment_plan', models.BooleanField(default=False)), + ('levy', models.BooleanField(default=False)), + ('garnishment', models.BooleanField(default=False)), + ('best_callback_time', models.CharField(blank=True, max_length=120)), + ('agent_notes', models.TextField(blank=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': 'pilot call', + 'verbose_name_plural': 'pilot calls', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/core/migrations/0002_webhookevent.py b/core/migrations/0002_webhookevent.py new file mode 100644 index 0000000..3354807 --- /dev/null +++ b/core/migrations/0002_webhookevent.py @@ -0,0 +1,38 @@ +# Generated by Django 5.2.7 on 2026-07-03 19:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='WebhookEvent', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('provider', models.CharField(default='generic', max_length=60)), + ('event_type', models.CharField(blank=True, max_length=120)), + ('external_id', models.CharField(blank=True, max_length=160)), + ('delivery_id', models.CharField(blank=True, max_length=160)), + ('source_ip', models.GenericIPAddressField(blank=True, null=True)), + ('content_type', models.CharField(blank=True, max_length=180)), + ('headers', models.JSONField(blank=True, default=dict)), + ('query_params', models.JSONField(blank=True, default=dict)), + ('payload', models.JSONField(blank=True, default=dict)), + ('raw_body', models.TextField(blank=True)), + ('processed', models.BooleanField(default=False)), + ('notes', models.TextField(blank=True)), + ('received_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name': 'webhook event', + 'verbose_name_plural': 'webhook events', + 'ordering': ['-received_at'], + 'indexes': [models.Index(fields=['provider', 'event_type'], name='core_webhoo_provide_870e6a_idx'), models.Index(fields=['received_at'], name='core_webhoo_receive_97109f_idx')], + }, + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45ff5370a9511099ad3ef373c5f214a409285510 GIT binary patch literal 4478 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 E|5^*1Z2$lO literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc index 799581567d2858c54584d0675ede42b2d728f5cb..06891647a57b764b56f1f798955b89cf5be01caa 100644 GIT binary patch delta 20 acmZ3%xPp;qIWI340}yQE^2nOVGY0@LH3ZcF delta 20 acmZ3%xPp;qIWI340}#~ttjL_mGY0@N6$L&3 diff --git a/core/models.py b/core/models.py index 71a8362..cfde08d 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,151 @@ -from django.db import models +from decimal import Decimal -# Create your models here. +from django.db import models +from django.urls import reverse + + +class PilotCall(models.Model): + SOURCE_YTEL = 'ytel' + SOURCE_RINGCENTRAL = 'ringcentral' + SOURCE_MANUAL = 'manual' + + CALL_SOURCE_CHOICES = [ + (SOURCE_YTEL, 'Ytel'), + (SOURCE_RINGCENTRAL, 'RingCentral fallback'), + (SOURCE_MANUAL, 'Manual/recording test'), + ] + + STATUS_UNCONFIRMED = 'unconfirmed' + STATUS_AVAILABLE = 'available' + STATUS_BLOCKED = 'blocked' + STATUS_PARTNER = 'partner' + + INTEGRATION_STATUS_CHOICES = [ + (STATUS_UNCONFIRMED, 'Not confirmed'), + (STATUS_AVAILABLE, 'Available'), + (STATUS_BLOCKED, 'Blocked / not exposed'), + (STATUS_PARTNER, 'Needs enterprise/partner access'), + ] + + client_name = models.CharField(max_length=140) + phone = models.CharField(max_length=40) + email = models.EmailField(blank=True) + call_source = models.CharField(max_length=24, choices=CALL_SOURCE_CHOICES, default=SOURCE_YTEL) + ytel_audio_status = models.CharField(max_length=24, choices=INTEGRATION_STATUS_CHOICES, default=STATUS_UNCONFIRMED) + logics_api_status = models.CharField(max_length=24, choices=INTEGRATION_STATUS_CHOICES, default=STATUS_UNCONFIRMED) + + transcript = models.TextField(help_text='Paste a live transcript segment or recorded-call transcript for the pilot.') + irs_debt = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) + state_debt = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) + tax_years = models.CharField(max_length=120, blank=True) + filing_status = models.CharField(max_length=80, blank=True) + employment_type = models.CharField(max_length=120, blank=True) + occupation = models.CharField(max_length=120, blank=True) + monthly_gross_income = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) + spouse_income = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) + mortgage = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) + rent = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) + utilities = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) + car_payment = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) + insurance = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) + student_loans = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) + credit_cards = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) + assets = models.TextField(blank=True) + business_expenses = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) + irs_payment_plan = models.BooleanField(default=False) + levy = models.BooleanField(default=False) + garnishment = models.BooleanField(default=False) + best_callback_time = models.CharField(max_length=120, blank=True) + agent_notes = models.TextField(blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ['-created_at'] + verbose_name = 'pilot call' + verbose_name_plural = 'pilot calls' + + def __str__(self): + return f'{self.client_name} · {self.get_call_source_display()}' + + def get_absolute_url(self): + return reverse('call_detail', args=[self.pk]) + + @property + def required_fields(self): + return [ + ('IRS debt', self.irs_debt), + ('Tax years', self.tax_years), + ('Employment', self.employment_type), + ('Monthly income', self.monthly_gross_income), + ('Best callback', self.best_callback_time), + ] + + @property + def completion_score(self): + if not self.required_fields: + return 0 + completed = sum(1 for _label, value in self.required_fields if self._has_value(value)) + return round((completed / len(self.required_fields)) * 100) + + @property + def missing_required_labels(self): + return [label for label, value in self.required_fields if not self._has_value(value)] + + @property + def urgent_flags(self): + flags = [] + if self.levy: + flags.append('Levy mentioned') + if self.garnishment: + flags.append('Wage garnishment mentioned') + if self.irs_debt and self.irs_debt > Decimal('50000'): + flags.append('Debt over $50,000') + if self.monthly_gross_income is not None and self.monthly_gross_income <= 0: + flags.append('No monthly income captured') + return flags + + @staticmethod + def _has_value(value): + if value is None: + return False + if isinstance(value, str): + return bool(value.strip()) + return True + + + +class WebhookEvent(models.Model): + PROVIDER_GENERIC = 'generic' + + provider = models.CharField(max_length=60, default=PROVIDER_GENERIC) + event_type = models.CharField(max_length=120, blank=True) + external_id = models.CharField(max_length=160, blank=True) + delivery_id = models.CharField(max_length=160, blank=True) + source_ip = models.GenericIPAddressField(null=True, blank=True) + content_type = models.CharField(max_length=180, blank=True) + headers = models.JSONField(default=dict, blank=True) + query_params = models.JSONField(default=dict, blank=True) + payload = models.JSONField(default=dict, blank=True) + raw_body = models.TextField(blank=True) + processed = models.BooleanField(default=False) + notes = models.TextField(blank=True) + received_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-received_at'] + indexes = [ + models.Index(fields=['provider', 'event_type']), + models.Index(fields=['received_at']), + ] + verbose_name = 'webhook event' + verbose_name_plural = 'webhook events' + + def __str__(self): + label = self.event_type or self.external_id or f'event #{self.pk}' + return f'{self.provider}: {label}' + + @property + def display_event_type(self): + return self.event_type or 'unclassified' diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..526ca8a 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,66 @@ +{% load static %} - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} + + {% block title %}{{ project_name|default:"ReliefSignal" }}{% endblock %} + {% if project_image_url %} {% endif %} - {% load static %} + + + + {% block head %}{% endblock %} - {% block content %}{% endblock %} + + + + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} + +
+ {% block content %}{% endblock %} +
+ +
+
+ ReliefSignal pilot console for tax relief call intake. + Real-time audio first · CRM sync ready · Completion scored +
+
+ diff --git a/core/templates/core/call_detail.html b/core/templates/core/call_detail.html new file mode 100644 index 0000000..2d6c4bd --- /dev/null +++ b/core/templates/core/call_detail.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} + +{% block title %}{{ call.client_name }} — Pilot Call Detail{% endblock %} +{% block meta_description %}Detailed tax relief pilot call record with transcript, extracted intake fields, urgency rules, completion score, and integration readiness.{% endblock %} + +{% block content %} +
+
+
+
+ Pilot call review +

{{ call.client_name }}

+

{{ call.phone }}{% if call.email %} · {{ call.email }}{% endif %}

+
+
{{ call.completion_score }}%complete
+
+
+
+ +
+
+
+
+
+
Transcript

Conversation feed

+
{{ call.transcript|linebreaks }}
+
+
+
Schema

Extracted intake fields

+
+ {% for label, value in intake_fields %} +
+
+ {{ label }} + {% if value %}{{ value }}{% else %}Missing{% endif %} +
+
+ {% endfor %} +
+
+
+ +
+
+
+{% endblock %} diff --git a/core/templates/core/call_form.html b/core/templates/core/call_form.html new file mode 100644 index 0000000..0416b5d --- /dev/null +++ b/core/templates/core/call_form.html @@ -0,0 +1,94 @@ +{% extends "base.html" %} + +{% block title %}New Pilot Call — ReliefSignal{% endblock %} +{% block meta_description %}Create a tax relief pilot call intake with transcript, extraction fields, integration readiness, and completion scoring.{% endblock %} + +{% block content %} +
+
+ Create pilot intake +

Start a real-time call proof point.

+

Paste a transcript segment, capture the tax relief fields your AI should extract, and record whether Ytel audio + Logics updates are ready.

+
+
+ +
+
+
+ {% csrf_token %} + {% if form.non_field_errors %} +
{{ form.non_field_errors }}
+ {% endif %} + +
+
01

Call identity

+
+ {% for field in form %} + {% if field.name in 'client_name phone email call_source ytel_audio_status logics_api_status' %} +
+ + {{ field }} + {% for error in field.errors %}
{{ error }}
{% endfor %} +
+ {% endif %} + {% endfor %} +
+
+ +
+
02

Transcript

+ + {{ form.transcript }} +

For the live MVP, this represents the text coming from speech-to-text every few seconds.

+ {% for error in form.transcript.errors %}
{{ error }}
{% endfor %} +
+ +
+
03

Extracted intake schema

+
+ {% for field in form %} + {% if field.name not in 'client_name phone email call_source ytel_audio_status logics_api_status transcript irs_payment_plan levy garnishment assets agent_notes' %} +
+ + {{ field }} + {% for error in field.errors %}
{{ error }}
{% endfor %} +
+ {% endif %} + {% endfor %} +
+ + {{ form.assets }} +
+
+
+ +
+
04

Rules engine flags

+
+
+
+ {% for field in form %} + {% if field.name in 'irs_payment_plan levy garnishment' %} + + {% endif %} + {% endfor %} +
+
+
+ + {{ form.agent_notes }} +
+
+
+ +
+ Cancel + +
+
+
+
+{% endblock %} diff --git a/core/templates/core/call_list.html b/core/templates/core/call_list.html new file mode 100644 index 0000000..801fb02 --- /dev/null +++ b/core/templates/core/call_list.html @@ -0,0 +1,64 @@ +{% extends "base.html" %} + +{% block title %}Pilot Call History — ReliefSignal{% endblock %} +{% block meta_description %}Review saved tax relief pilot calls with source, completion score, CRM readiness, and urgency flags.{% endblock %} + +{% block content %} +
+
+
+ History +

Pilot call records

+

Every saved call becomes evidence for what the MVP captures and what integrations still need validation.

+
+ New pilot call +
+
+ +
+
+ {% if calls %} +
+
+ + + + + + + + + + + + + {% for call in calls %} + + + + + + + + + {% endfor %} + +
ClientSourceCompletionReadinessFlagsOpen
{{ call.client_name }}
{{ call.phone }}
{{ call.get_call_source_display }}
{{ call.created_at|date:"M j, Y g:i A" }}
{{ call.completion_score }}%Ytel: {{ call.get_ytel_audio_status_display }}
Logics: {{ call.get_logics_api_status_display }}
+ {% if call.urgent_flags %} + {% for flag in call.urgent_flags %}{{ flag }}{% endfor %} + {% else %} + None + {% endif %} + Review
+
+
+ {% else %} +
+

No call history yet

+

Save your first pilot call to confirm the workflow from transcript capture to scored intake review.

+ Create first call +
+ {% endif %} +
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..92d9f1a 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,104 @@ {% extends "base.html" %} -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% block title %}ReliefSignal — Real-Time Tax Relief Intake Pilot{% endblock %} +{% block meta_description %}A polished MVP console for validating real-time Ytel audio readiness, Logics CRM sync, and AI-assisted tax relief intake completion.{% endblock %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+
+
+
+
+
+ Real-time tax relief intake MVP +

Prove live call extraction before building the whole platform.

+

ReliefSignal turns each pilot call into a structured intake: transcript, IRS debt fields, urgency flags, completion score, and Ytel/Logics readiness notes in one workflow.

+ +
+
{{ total_calls }}Pilot calls
+
{{ avg_completion }}%Avg completion
+
{{ urgent_count }}Urgent flags
+
+
+
+
+
+
+ + Live Call Screen +
+ Ytel audio: validate +
+
+

Client: I owe around $24,000 to the IRS and I’m worried about a levy.

+

Agent: Which years are included, and what is your monthly income?

+
+
+
IRS Debt$24,000
+
Employment1099
+
AlertLevy
+
Completion80%
+
+
+
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC) -
-{% endblock %} \ No newline at end of file + + +
+
+
+
+
First workflow
+

Capture → score → review.

+

This first iteration intentionally keeps the media integration mocked, but the workflow stores real records so a tax office can test the intake logic immediately.

+
+
+
+
01

Create

Paste live transcript text and extracted intake values.

+
02

Validate

Track Ytel audio and Logics API readiness per pilot call.

+
03

Coach

Surface missing fields and urgent tax relief flags.

+
+
+
+
+
+ +
+
+
+
+
Recent pilot calls
+

History starts here.

+
+ Open history +
+ {% if recent_calls %} + + {% else %} +
+

No pilot calls yet

+

Create the first call record to test transcript capture, intake fields, completion scoring, and urgent flags.

+ Create first call +
+ {% endif %} +
+
+{% endblock %} diff --git a/core/templates/core/webhook_list.html b/core/templates/core/webhook_list.html new file mode 100644 index 0000000..d47b124 --- /dev/null +++ b/core/templates/core/webhook_list.html @@ -0,0 +1,94 @@ +{% extends "base.html" %} + +{% block title %}Webhook Receiver — ReliefSignal{% endblock %} +{% block meta_description %}Provider-neutral webhook receiver for testing external call-event payloads before adapting the integration to Ytel.{% endblock %} + +{% block content %} +
+
+ Generic webhook inbox +
+
+

Capture provider events before we commit to Ytel-specific logic.

+

Use this receiver to collect real webhook payloads, inspect headers safely, and map the fields we need for call intake automation.

+
+ +
+
+
+ +
+
+
+
+
+
+ 01 +

Receiver endpoint

+
+

Point test providers here. Add ?provider=ytel when you start sending Ytel samples.

+
+ POST endpoint + {{ endpoint_url }} +
+
curl -X POST "{{ endpoint_url }}?provider=test" \
+  -H "Content-Type: application/json" \
+  -d '{"event_type":"call.ended","external_id":"demo-call-001","status":"completed"}'
+
    +
  • JSON bodiesAccepted
  • +
  • Form postsAccepted
  • +
  • Auth/signature headersRedacted
  • +
  • Max body size512 KB
  • +
+
+
+ +
+
+
+
+ {{ total_events }} total received +

Recent webhook events

+
+ Open in admin +
+ + {% if events %} +
+ + + + + + + + + + + + {% for event in events %} + + + + + + + + {% endfor %} + +
ReceivedProviderEventExternal IDPreview
#{{ event.pk }}
{{ event.received_at|date:"M j, Y g:i A" }}
{{ event.provider }}{{ event.display_event_type }}{% if event.external_id %}{{ event.external_id }}{% else %}Not supplied{% endif %}{{ event.raw_body|default:"No body"|truncatechars:110 }}
+
+ {% else %} +
+

No webhooks received yet

+

Send a test POST to the endpoint to verify connectivity, then we can map the real Ytel fields into pilot call records.

+
+ {% endif %} +
+
+
+
+
+{% endblock %} diff --git a/core/urls.py b/core/urls.py index 6299e3d..adf3301 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,12 @@ from django.urls import path -from .views import home +from .views import call_create, call_detail, call_list, home, webhook_generic, webhook_list urlpatterns = [ - path("", home, name="home"), + path('', home, name='home'), + path('calls/', call_list, name='call_list'), + path('calls/new/', call_create, name='call_create'), + path('calls//', call_detail, name='call_detail'), + path('webhooks/', webhook_list, name='webhook_list'), + path('webhooks/generic/', webhook_generic, name='webhook_generic'), ] diff --git a/core/views.py b/core/views.py index c9aed12..54a130c 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,277 @@ -import os -import platform +import ipaddress +import json +import logging -from django import get_version as django_version -from django.shortcuts import render -from django.utils import timezone +from django.contrib import messages +from django.db.models import Count +from django.http import JsonResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_http_methods + +from .forms import PilotCallForm +from .models import PilotCall, WebhookEvent + + +logger = logging.getLogger(__name__) +MAX_WEBHOOK_BODY_BYTES = 512 * 1024 def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() + """Render the branded landing dashboard for the real-time intake pilot.""" + recent_calls = PilotCall.objects.all()[:3] + total_calls = PilotCall.objects.count() + urgent_count = sum(1 for call in PilotCall.objects.all() if call.urgent_flags) + avg_completion = 0 + if total_calls: + avg_completion = round(sum(call.completion_score for call in PilotCall.objects.all()) / total_calls) context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + 'project_name': 'ReliefSignal', + 'meta_description': 'Real-time tax relief call intake pilot for live transcription, structured extraction, completion scoring, and CRM readiness.', + 'recent_calls': recent_calls, + 'total_calls': total_calls, + 'urgent_count': urgent_count, + 'avg_completion': avg_completion, } - return render(request, "core/index.html", context) + return render(request, 'core/index.html', context) + + +def call_list(request): + calls = PilotCall.objects.all() + summary = calls.aggregate(total=Count('id')) + context = { + 'project_name': 'ReliefSignal', + 'meta_description': 'Review tax relief pilot calls, completion scores, and urgency flags.', + 'calls': calls, + 'total_calls': summary['total'] or 0, + } + return render(request, 'core/call_list.html', context) + + +def call_create(request): + if request.method == 'POST': + form = PilotCallForm(request.POST) + if form.is_valid(): + call = form.save() + messages.success(request, 'Pilot call saved. Review the completion score and missing intake fields below.') + return redirect('call_detail', pk=call.pk) + else: + form = PilotCallForm() + + context = { + 'project_name': 'ReliefSignal', + 'meta_description': 'Create a tax relief pilot call intake from a live transcript segment.', + 'form': form, + } + return render(request, 'core/call_form.html', context) + + +def call_detail(request, pk): + call = get_object_or_404(PilotCall, pk=pk) + intake_fields = [ + ('IRS Debt', call.irs_debt), + ('State Debt', call.state_debt), + ('Tax Years', call.tax_years), + ('Filing Status', call.filing_status), + ('Employment', call.employment_type), + ('Occupation', call.occupation), + ('Monthly Gross Income', call.monthly_gross_income), + ('Spouse Income', call.spouse_income), + ('Mortgage', call.mortgage), + ('Rent', call.rent), + ('Utilities', call.utilities), + ('Car Payment', call.car_payment), + ('Insurance', call.insurance), + ('Student Loans', call.student_loans), + ('Credit Cards', call.credit_cards), + ('Business Expenses', call.business_expenses), + ('Best Callback Time', call.best_callback_time), + ] + context = { + 'project_name': 'ReliefSignal', + 'meta_description': f'Pilot call detail for {call.client_name}: transcript, extracted tax relief fields, urgency flags, and completion score.', + 'call': call, + 'intake_fields': intake_fields, + } + return render(request, 'core/call_detail.html', context) + + + +def webhook_list(request): + events = WebhookEvent.objects.all()[:25] + context = { + 'project_name': 'ReliefSignal', + 'meta_description': 'Provider-neutral webhook inbox for testing Ytel-style call events before a dedicated integration is finalized.', + 'events': events, + 'total_events': WebhookEvent.objects.count(), + 'endpoint_url': _public_absolute_uri(request, reverse('webhook_generic')), + } + return render(request, 'core/webhook_list.html', context) + + +@csrf_exempt +@require_http_methods(['GET', 'POST']) +def webhook_generic(request): + if request.method == 'GET': + return JsonResponse({ + 'ok': True, + 'name': 'ReliefSignal generic webhook receiver', + 'usage': 'POST JSON or form payloads to this endpoint. Add ?provider=ytel later when testing Ytel deliveries.', + 'accepted_methods': ['POST'], + }) + + body = request.body or b'' + if len(body) > MAX_WEBHOOK_BODY_BYTES: + logger.warning('Rejected oversized webhook body: %s bytes', len(body)) + return JsonResponse({'ok': False, 'error': 'Payload is too large.'}, status=413) + + content_type = request.META.get('CONTENT_TYPE', '')[:180] + raw_body = body.decode('utf-8', errors='replace') + payload, parsed = _parse_webhook_payload(request, raw_body, content_type) + + provider = _normalise_label( + request.GET.get('provider') + or request.headers.get('X-Webhook-Provider') + or _payload_lookup(payload, 'provider', 'source'), + fallback='generic', + max_length=60, + ) + event_type = _normalise_label( + request.headers.get('X-Event-Type') + or request.headers.get('X-Webhook-Event') + or request.headers.get('X-Hook-Event') + or request.headers.get('X-Ytel-Event-Type') + or _payload_lookup(payload, 'event_type', 'eventType', 'event', 'type', 'action', 'status'), + fallback='', + max_length=120, + ) + delivery_id = _normalise_label( + request.headers.get('X-Delivery-Id') + or request.headers.get('X-Webhook-Id') + or request.headers.get('X-Request-Id') + or request.headers.get('X-Event-Id') + or request.headers.get('X-Ytel-Delivery-Id') + or request.headers.get('X-Ytel-Event-Id') + or request.headers.get('Cf-Ray'), + fallback='', + max_length=160, + ) + external_id = _normalise_label( + _payload_lookup(payload, 'external_id', 'externalId', 'event_id', 'eventId', 'id', 'call_id', 'callId', 'uuid', 'sid'), + fallback='', + max_length=160, + ) + + event = WebhookEvent.objects.create( + provider=provider, + event_type=event_type, + external_id=external_id, + delivery_id=delivery_id, + source_ip=_source_ip(request), + content_type=content_type, + headers=_safe_request_headers(request), + query_params=_query_params(request), + payload=payload, + raw_body=raw_body, + ) + + return JsonResponse({ + 'ok': True, + 'accepted': True, + 'parsed': parsed, + 'id': event.pk, + 'provider': event.provider, + 'event_type': event.display_event_type, + 'received_at': event.received_at.isoformat(), + }, status=202) + + +def _public_absolute_uri(request, path): + url = request.build_absolute_uri(path) + host = request.get_host().split(':')[0] + if host not in ('127.0.0.1', 'localhost') and url.startswith('http://'): + return 'https://' + url[len('http://'):] + return url + + +def _parse_webhook_payload(request, raw_body, content_type): + if raw_body and ('application/json' in content_type or raw_body.lstrip().startswith(('{', '['))): + try: + return json.loads(raw_body), True + except json.JSONDecodeError: + return {'_unparsed_preview': raw_body[:2000]}, False + + if request.POST: + return _multi_value_dict(request.POST), True + + if raw_body: + return {'_raw_preview': raw_body[:2000]}, False + + return {}, True + + +def _safe_request_headers(request): + sensitive_markers = ('authorization', 'cookie', 'token', 'secret', 'key', 'signature') + safe_headers = {} + for name, value in request.headers.items(): + if any(marker in name.lower() for marker in sensitive_markers): + safe_headers[name] = '[redacted]' + else: + safe_headers[name] = str(value)[:500] + return safe_headers + + +def _query_params(request): + return _multi_value_dict(request.GET) + + +def _multi_value_dict(query_dict): + return { + key: values if len(values := query_dict.getlist(key)) > 1 else query_dict.get(key) + for key in query_dict + } + + +def _payload_lookup(payload, *keys): + if not isinstance(payload, dict): + return None + + for key in keys: + value = payload.get(key) + if value not in (None, ''): + return value + + for nested_key in ('data', 'event', 'call', 'payload'): + nested = payload.get(nested_key) + if isinstance(nested, dict): + value = _payload_lookup(nested, *keys) + if value not in (None, ''): + return value + + return None + + +def _normalise_label(value, fallback='', max_length=120): + if value in (None, ''): + return fallback + if isinstance(value, (dict, list)): + value = json.dumps(value, sort_keys=True) + return str(value).strip()[:max_length] or fallback + + +def _source_ip(request): + raw_ip = ( + request.headers.get('Cf-Connecting-Ip') + or request.headers.get('X-Real-Ip') + or request.headers.get('X-Forwarded-For') + or request.META.get('REMOTE_ADDR') + or '' + ) + candidate = raw_ip.split(',')[0].strip() + try: + ipaddress.ip_address(candidate) + except ValueError: + return None + return candidate diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..7186a01 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,334 @@ -/* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +/* ReliefSignal custom brand layer — intentionally overrides default Bootstrap appearance. */ +:root { + --rs-ink: #10221f; + --rs-muted: #5f716d; + --rs-primary: #0f766e; + --rs-secondary: #f97316; + --rs-accent: #14b8a6; + --rs-cream: #fff8ed; + --rs-mint: #e7fbf5; + --rs-surface: #ffffff; + --rs-line: rgba(16, 34, 31, 0.12); + --rs-shadow: 0 24px 80px rgba(15, 118, 110, 0.16); } + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +body { + margin: 0; + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: linear-gradient(180deg, #fffdf7 0%, #f5fbf7 54%, #fff8ed 100%); + color: var(--rs-ink); + min-height: 100vh; +} + +.skip-link { + position: absolute; + left: 1rem; + top: -4rem; + z-index: 2000; + padding: .75rem 1rem; + background: var(--rs-ink); + color: #fff; + border-radius: 999px; + transition: top .2s ease; +} +.skip-link:focus { top: 1rem; } + +.app-nav { + background: rgba(255, 253, 247, 0.82); + backdrop-filter: blur(18px); + border-bottom: 1px solid rgba(16, 34, 31, 0.08); +} + +.brand-mark { + display: inline-flex; + align-items: center; + gap: .65rem; + font-weight: 800; + letter-spacing: -0.03em; + color: var(--rs-ink) !important; +} + +.brand-icon { + display: inline-grid; + place-items: center; + width: 2.35rem; + height: 2.35rem; + border-radius: 1rem; + color: #fff; + background: linear-gradient(135deg, var(--rs-primary), var(--rs-secondary)); + box-shadow: 0 12px 30px rgba(15, 118, 110, 0.28); + font-size: .82rem; +} + +.nav-link { + color: rgba(16, 34, 31, 0.72) !important; + font-weight: 700; +} +.nav-link:hover, .nav-link:focus { color: var(--rs-primary) !important; } + +.btn { + border-radius: 999px; + font-weight: 800; + padding: .85rem 1.3rem; +} +.btn-sm { padding: .45rem .85rem; } +.btn-signal { + background: linear-gradient(135deg, var(--rs-primary), #0d9488); + color: #fff; + border: 0; + box-shadow: 0 14px 34px rgba(15, 118, 110, 0.24); +} +.btn-signal:hover, .btn-signal:focus { + color: #fff; + transform: translateY(-1px); + box-shadow: 0 18px 42px rgba(15, 118, 110, 0.30); +} +.btn-ghost, .btn-outline-signal { + border: 1px solid rgba(15, 118, 110, .22); + color: var(--rs-primary); + background: rgba(255,255,255,.68); +} +.btn-ghost:hover, .btn-outline-signal:hover { background: var(--rs-mint); color: var(--rs-ink); } + +.message-stack { position: fixed; top: 5.5rem; right: 0; left: 0; z-index: 1080; pointer-events: none; } +.message-stack .alert { max-width: 760px; margin-left: auto; pointer-events: auto; border-radius: 1rem; } + +.hero-section { + background: + radial-gradient(circle at 16% 10%, rgba(249, 115, 22, .18), transparent 28%), + radial-gradient(circle at 88% 14%, rgba(20, 184, 166, .22), transparent 30%), + linear-gradient(135deg, #fff8ed 0%, #ecfff9 100%); +} +.py-lg-6 { padding-top: 6rem !important; padding-bottom: 6rem !important; } + +.orb { + position: absolute; + border-radius: 999px; + filter: blur(2px); + opacity: .75; +} +.orb-one { width: 18rem; height: 18rem; background: rgba(249,115,22,.16); left: -6rem; top: 6rem; } +.orb-two { width: 22rem; height: 22rem; background: rgba(20,184,166,.18); right: -8rem; bottom: -5rem; } + +.eyebrow, .section-kicker, .small-label { + display: inline-flex; + align-items: center; + gap: .45rem; + text-transform: uppercase; + letter-spacing: .14em; + font-size: .77rem; + font-weight: 900; + color: var(--rs-primary); +} + +.display-title, .page-title, .section-title { + font-family: "Fraunces", Georgia, serif; + color: var(--rs-ink); + letter-spacing: -0.045em; + line-height: .98; +} +.display-title { font-size: clamp(3rem, 7vw, 6.6rem); max-width: 960px; } +.page-title { font-size: clamp(2.5rem, 5vw, 4.8rem); max-width: 980px; } +.section-title { font-size: clamp(2rem, 4vw, 3.4rem); } +.hero-copy { + max-width: 720px; + color: var(--rs-muted); + font-size: clamp(1.05rem, 1.4vw, 1.28rem); + line-height: 1.7; +} +.text-muted-custom { color: var(--rs-muted); } + +.hero-proof { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} +.hero-proof div { + min-width: 135px; + padding: 1rem 1.15rem; + border: 1px solid var(--rs-line); + border-radius: 1.25rem; + background: rgba(255,255,255,.64); +} +.hero-proof strong { display: block; font-size: 1.7rem; color: var(--rs-primary); } +.hero-proof span { color: var(--rs-muted); font-size: .88rem; font-weight: 700; } + +.glass-card, .form-panel, .detail-panel, .history-shell, .empty-state, .call-card, .step-card { + background: rgba(255, 255, 255, 0.78); + border: 1px solid rgba(16, 34, 31, 0.10); + box-shadow: var(--rs-shadow); + backdrop-filter: blur(16px); +} +.live-card { border-radius: 2rem; padding: 1.3rem; transform: rotate(1deg); } +.pulse-dot { + display: inline-block; + width: .78rem; + height: .78rem; + margin-right: .45rem; + border-radius: 999px; + background: var(--rs-secondary); + box-shadow: 0 0 0 .42rem rgba(249,115,22,.16); +} +.status-pill, .score-badge, .mini-flag, .alert-flag { + display: inline-flex; + align-items: center; + border-radius: 999px; + font-size: .78rem; + font-weight: 900; +} +.status-pill { padding: .5rem .75rem; color: #8a3d00; background: #fff0d8; } +.score-badge { padding: .55rem .7rem; color: #07564f; background: var(--rs-mint); } + +.transcript-box, .transcript-review { + border-radius: 1.3rem; + background: #10221f; + color: #eaffe9; + padding: 1rem; + line-height: 1.65; +} +.transcript-box p:last-child, .transcript-review p:last-child { margin-bottom: 0; } +.metric-tile, .field-tile { + border-radius: 1.2rem; + padding: 1rem; + background: #fffdf8; + border: 1px solid var(--rs-line); +} +.metric-tile span, .field-tile span { display: block; color: var(--rs-muted); font-size: .82rem; font-weight: 800; } +.metric-tile strong, .field-tile strong { display: block; color: var(--rs-ink); font-size: 1.25rem; margin-top: .25rem; } +.metric-tile.urgent { background: #fff0d8; border-color: rgba(249,115,22,.35); } + +.section-pad { padding: 5rem 0; } +.step-card, .call-card, .empty-state { border-radius: 1.5rem; padding: 1.35rem; height: 100%; } +.step-card span { color: var(--rs-secondary); font-weight: 900; } +.step-card h3, .call-card h3 { margin: .7rem 0 .45rem; font-weight: 900; letter-spacing: -.03em; } +.step-card p, .call-card p { color: var(--rs-muted); } +.call-card-link { color: inherit; text-decoration: none; display: block; height: 100%; } +.call-card-link:hover .call-card { transform: translateY(-3px); border-color: rgba(15,118,110,.28); } +.call-card, .btn, .step-card { transition: .18s ease; } +.empty-state { text-align: center; max-width: 760px; margin: 0 auto; padding: 3rem; } + +.page-hero { + background: linear-gradient(135deg, #fff8ed 0%, #ecfff9 100%); + padding: 5rem 0 3rem; + border-bottom: 1px solid rgba(16,34,31,.08); +} +.page-hero.compact { padding-top: 4rem; } +.detail-hero { background: radial-gradient(circle at 78% 10%, rgba(249,115,22,.15), transparent 25%), linear-gradient(135deg, #ecfff9, #fff8ed); } + +.form-panel, .detail-panel, .history-shell { border-radius: 1.6rem; padding: 1.4rem; } +.panel-heading { + display: flex; + align-items: baseline; + gap: .8rem; + margin-bottom: 1.2rem; +} +.panel-heading span { color: var(--rs-secondary); font-weight: 900; letter-spacing: .08em; text-transform: uppercase; font-size: .78rem; } +.panel-heading h2 { margin: 0; font-size: 1.25rem; font-weight: 900; letter-spacing: -.03em; } +.form-label { font-weight: 850; color: var(--rs-ink); } +.form-control, .form-select { + border-radius: 1rem; + border: 1px solid rgba(16,34,31,.14); + padding: .85rem 1rem; + background-color: rgba(255,255,255,.82); +} +.form-control:focus, .form-select:focus { + border-color: var(--rs-accent); + box-shadow: 0 0 0 .22rem rgba(20,184,166,.18); +} +.form-hint, .invalid-note { color: var(--rs-muted); font-size: .9rem; margin-top: .5rem; } +.invalid-note { color: #b42318; font-weight: 800; } +.checks-grid { display: grid; gap: .9rem; } +.signal-check { + display: flex; + align-items: center; + gap: .75rem; + padding: .85rem 1rem; + border-radius: 1rem; + background: #fffdf8; + border: 1px solid var(--rs-line); + font-weight: 850; +} +.form-check-input:checked { background-color: var(--rs-primary); border-color: var(--rs-primary); } + +.history-table { margin: 0; } +.history-table thead th { + color: var(--rs-muted); + text-transform: uppercase; + letter-spacing: .08em; + font-size: .75rem; + border-bottom-color: var(--rs-line); +} +.history-table td { padding: 1rem .75rem; border-color: rgba(16,34,31,.08); } +.history-table td span { color: var(--rs-muted); font-size: .9rem; } +.mini-flag { padding: .35rem .55rem; margin: .1rem; color: #8a3d00; background: #fff0d8; } +.alert-flag { padding: .65rem .8rem; color: #8a3d00; background: #fff0d8; } +.flag-stack { display: flex; flex-wrap: wrap; gap: .5rem; } + +.score-circle { + width: 8.25rem; + height: 8.25rem; + border-radius: 999px; + display: grid; + place-items: center; + text-align: center; + background: conic-gradient(var(--rs-primary), var(--rs-accent), var(--rs-secondary), var(--rs-primary)); + color: #fff; + box-shadow: var(--rs-shadow); +} +.score-circle strong { display: block; font-size: 2rem; line-height: 1; } +.score-circle span { display: block; font-size: .82rem; font-weight: 900; text-transform: uppercase; letter-spacing: .08em; } +.field-tile.missing { background: #fff7ed; border-color: rgba(249,115,22,.28); } +.field-tile.missing strong { color: #9a3412; } +.readiness-list, .missing-list { list-style: none; padding: 0; margin: 0; display: grid; gap: .8rem; } +.readiness-list li, .missing-list li { + display: flex; + justify-content: space-between; + gap: 1rem; + padding: .85rem 0; + border-bottom: 1px solid rgba(16,34,31,.08); +} +.readiness-list span { color: var(--rs-muted); font-weight: 800; } +.readiness-list strong { text-align: right; } +.missing-list li { display: block; color: #9a3412; font-weight: 900; } + +.site-footer { + padding: 2rem 0; + color: var(--rs-muted); + border-top: 1px solid rgba(16,34,31,.08); + background: rgba(255,253,247,.74); + font-weight: 700; +} + +@media (max-width: 767px) { + .section-pad { padding: 3.25rem 0; } + .live-card { transform: none; } + .hero-proof div { flex: 1 1 100%; } + .empty-state { padding: 2rem 1.2rem; } + .score-circle { width: 7rem; height: 7rem; } +} + + +.endpoint-box { + display: grid; + gap: .45rem; + border-radius: 1.1rem; + padding: 1rem; + background: #fffdf8; + border: 1px solid var(--rs-line); +} +.endpoint-box span { color: var(--rs-muted); font-size: .78rem; font-weight: 900; letter-spacing: .08em; text-transform: uppercase; } +.endpoint-box code, .endpoint-code code, .webhook-preview { color: var(--rs-ink); white-space: pre-wrap; word-break: break-word; } +.endpoint-code { + border-radius: 1.15rem; + padding: 1rem; + background: #10221f; + color: #ecfff9; + border: 1px solid rgba(255,255,255,.08); + overflow-x: auto; +} +.endpoint-code code { color: #ecfff9; font-size: .88rem; } +.webhook-preview { display: inline-block; max-width: 18rem; } diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..7186a01 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,334 @@ - +/* ReliefSignal custom brand layer — intentionally overrides default Bootstrap appearance. */ :root { - --bg-color-start: #6a11cb; - --bg-color-end: #2575fc; - --text-color: #ffffff; - --card-bg-color: rgba(255, 255, 255, 0.01); - --card-border-color: rgba(255, 255, 255, 0.1); + --rs-ink: #10221f; + --rs-muted: #5f716d; + --rs-primary: #0f766e; + --rs-secondary: #f97316; + --rs-accent: #14b8a6; + --rs-cream: #fff8ed; + --rs-mint: #e7fbf5; + --rs-surface: #ffffff; + --rs-line: rgba(16, 34, 31, 0.12); + --rs-shadow: 0 24px 80px rgba(15, 118, 110, 0.16); } + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + body { margin: 0; - font-family: 'Inter', sans-serif; - background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); - color: var(--text-color); - display: flex; - justify-content: center; - align-items: center; + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: linear-gradient(180deg, #fffdf7 0%, #f5fbf7 54%, #fff8ed 100%); + color: var(--rs-ink); min-height: 100vh; - text-align: center; - overflow: hidden; - position: relative; } + +.skip-link { + position: absolute; + left: 1rem; + top: -4rem; + z-index: 2000; + padding: .75rem 1rem; + background: var(--rs-ink); + color: #fff; + border-radius: 999px; + transition: top .2s ease; +} +.skip-link:focus { top: 1rem; } + +.app-nav { + background: rgba(255, 253, 247, 0.82); + backdrop-filter: blur(18px); + border-bottom: 1px solid rgba(16, 34, 31, 0.08); +} + +.brand-mark { + display: inline-flex; + align-items: center; + gap: .65rem; + font-weight: 800; + letter-spacing: -0.03em; + color: var(--rs-ink) !important; +} + +.brand-icon { + display: inline-grid; + place-items: center; + width: 2.35rem; + height: 2.35rem; + border-radius: 1rem; + color: #fff; + background: linear-gradient(135deg, var(--rs-primary), var(--rs-secondary)); + box-shadow: 0 12px 30px rgba(15, 118, 110, 0.28); + font-size: .82rem; +} + +.nav-link { + color: rgba(16, 34, 31, 0.72) !important; + font-weight: 700; +} +.nav-link:hover, .nav-link:focus { color: var(--rs-primary) !important; } + +.btn { + border-radius: 999px; + font-weight: 800; + padding: .85rem 1.3rem; +} +.btn-sm { padding: .45rem .85rem; } +.btn-signal { + background: linear-gradient(135deg, var(--rs-primary), #0d9488); + color: #fff; + border: 0; + box-shadow: 0 14px 34px rgba(15, 118, 110, 0.24); +} +.btn-signal:hover, .btn-signal:focus { + color: #fff; + transform: translateY(-1px); + box-shadow: 0 18px 42px rgba(15, 118, 110, 0.30); +} +.btn-ghost, .btn-outline-signal { + border: 1px solid rgba(15, 118, 110, .22); + color: var(--rs-primary); + background: rgba(255,255,255,.68); +} +.btn-ghost:hover, .btn-outline-signal:hover { background: var(--rs-mint); color: var(--rs-ink); } + +.message-stack { position: fixed; top: 5.5rem; right: 0; left: 0; z-index: 1080; pointer-events: none; } +.message-stack .alert { max-width: 760px; margin-left: auto; pointer-events: auto; border-radius: 1rem; } + +.hero-section { + background: + radial-gradient(circle at 16% 10%, rgba(249, 115, 22, .18), transparent 28%), + radial-gradient(circle at 88% 14%, rgba(20, 184, 166, .22), transparent 30%), + linear-gradient(135deg, #fff8ed 0%, #ecfff9 100%); +} +.py-lg-6 { padding-top: 6rem !important; padding-bottom: 6rem !important; } + +.orb { + position: absolute; + border-radius: 999px; + filter: blur(2px); + opacity: .75; +} +.orb-one { width: 18rem; height: 18rem; background: rgba(249,115,22,.16); left: -6rem; top: 6rem; } +.orb-two { width: 22rem; height: 22rem; background: rgba(20,184,166,.18); right: -8rem; bottom: -5rem; } + +.eyebrow, .section-kicker, .small-label { + display: inline-flex; + align-items: center; + gap: .45rem; + text-transform: uppercase; + letter-spacing: .14em; + font-size: .77rem; + font-weight: 900; + color: var(--rs-primary); +} + +.display-title, .page-title, .section-title { + font-family: "Fraunces", Georgia, serif; + color: var(--rs-ink); + letter-spacing: -0.045em; + line-height: .98; +} +.display-title { font-size: clamp(3rem, 7vw, 6.6rem); max-width: 960px; } +.page-title { font-size: clamp(2.5rem, 5vw, 4.8rem); max-width: 980px; } +.section-title { font-size: clamp(2rem, 4vw, 3.4rem); } +.hero-copy { + max-width: 720px; + color: var(--rs-muted); + font-size: clamp(1.05rem, 1.4vw, 1.28rem); + line-height: 1.7; +} +.text-muted-custom { color: var(--rs-muted); } + +.hero-proof { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} +.hero-proof div { + min-width: 135px; + padding: 1rem 1.15rem; + border: 1px solid var(--rs-line); + border-radius: 1.25rem; + background: rgba(255,255,255,.64); +} +.hero-proof strong { display: block; font-size: 1.7rem; color: var(--rs-primary); } +.hero-proof span { color: var(--rs-muted); font-size: .88rem; font-weight: 700; } + +.glass-card, .form-panel, .detail-panel, .history-shell, .empty-state, .call-card, .step-card { + background: rgba(255, 255, 255, 0.78); + border: 1px solid rgba(16, 34, 31, 0.10); + box-shadow: var(--rs-shadow); + backdrop-filter: blur(16px); +} +.live-card { border-radius: 2rem; padding: 1.3rem; transform: rotate(1deg); } +.pulse-dot { + display: inline-block; + width: .78rem; + height: .78rem; + margin-right: .45rem; + border-radius: 999px; + background: var(--rs-secondary); + box-shadow: 0 0 0 .42rem rgba(249,115,22,.16); +} +.status-pill, .score-badge, .mini-flag, .alert-flag { + display: inline-flex; + align-items: center; + border-radius: 999px; + font-size: .78rem; + font-weight: 900; +} +.status-pill { padding: .5rem .75rem; color: #8a3d00; background: #fff0d8; } +.score-badge { padding: .55rem .7rem; color: #07564f; background: var(--rs-mint); } + +.transcript-box, .transcript-review { + border-radius: 1.3rem; + background: #10221f; + color: #eaffe9; + padding: 1rem; + line-height: 1.65; +} +.transcript-box p:last-child, .transcript-review p:last-child { margin-bottom: 0; } +.metric-tile, .field-tile { + border-radius: 1.2rem; + padding: 1rem; + background: #fffdf8; + border: 1px solid var(--rs-line); +} +.metric-tile span, .field-tile span { display: block; color: var(--rs-muted); font-size: .82rem; font-weight: 800; } +.metric-tile strong, .field-tile strong { display: block; color: var(--rs-ink); font-size: 1.25rem; margin-top: .25rem; } +.metric-tile.urgent { background: #fff0d8; border-color: rgba(249,115,22,.35); } + +.section-pad { padding: 5rem 0; } +.step-card, .call-card, .empty-state { border-radius: 1.5rem; padding: 1.35rem; height: 100%; } +.step-card span { color: var(--rs-secondary); font-weight: 900; } +.step-card h3, .call-card h3 { margin: .7rem 0 .45rem; font-weight: 900; letter-spacing: -.03em; } +.step-card p, .call-card p { color: var(--rs-muted); } +.call-card-link { color: inherit; text-decoration: none; display: block; height: 100%; } +.call-card-link:hover .call-card { transform: translateY(-3px); border-color: rgba(15,118,110,.28); } +.call-card, .btn, .step-card { transition: .18s ease; } +.empty-state { text-align: center; max-width: 760px; margin: 0 auto; padding: 3rem; } + +.page-hero { + background: linear-gradient(135deg, #fff8ed 0%, #ecfff9 100%); + padding: 5rem 0 3rem; + border-bottom: 1px solid rgba(16,34,31,.08); +} +.page-hero.compact { padding-top: 4rem; } +.detail-hero { background: radial-gradient(circle at 78% 10%, rgba(249,115,22,.15), transparent 25%), linear-gradient(135deg, #ecfff9, #fff8ed); } + +.form-panel, .detail-panel, .history-shell { border-radius: 1.6rem; padding: 1.4rem; } +.panel-heading { + display: flex; + align-items: baseline; + gap: .8rem; + margin-bottom: 1.2rem; +} +.panel-heading span { color: var(--rs-secondary); font-weight: 900; letter-spacing: .08em; text-transform: uppercase; font-size: .78rem; } +.panel-heading h2 { margin: 0; font-size: 1.25rem; font-weight: 900; letter-spacing: -.03em; } +.form-label { font-weight: 850; color: var(--rs-ink); } +.form-control, .form-select { + border-radius: 1rem; + border: 1px solid rgba(16,34,31,.14); + padding: .85rem 1rem; + background-color: rgba(255,255,255,.82); +} +.form-control:focus, .form-select:focus { + border-color: var(--rs-accent); + box-shadow: 0 0 0 .22rem rgba(20,184,166,.18); +} +.form-hint, .invalid-note { color: var(--rs-muted); font-size: .9rem; margin-top: .5rem; } +.invalid-note { color: #b42318; font-weight: 800; } +.checks-grid { display: grid; gap: .9rem; } +.signal-check { + display: flex; + align-items: center; + gap: .75rem; + padding: .85rem 1rem; + border-radius: 1rem; + background: #fffdf8; + border: 1px solid var(--rs-line); + font-weight: 850; +} +.form-check-input:checked { background-color: var(--rs-primary); border-color: var(--rs-primary); } + +.history-table { margin: 0; } +.history-table thead th { + color: var(--rs-muted); + text-transform: uppercase; + letter-spacing: .08em; + font-size: .75rem; + border-bottom-color: var(--rs-line); +} +.history-table td { padding: 1rem .75rem; border-color: rgba(16,34,31,.08); } +.history-table td span { color: var(--rs-muted); font-size: .9rem; } +.mini-flag { padding: .35rem .55rem; margin: .1rem; color: #8a3d00; background: #fff0d8; } +.alert-flag { padding: .65rem .8rem; color: #8a3d00; background: #fff0d8; } +.flag-stack { display: flex; flex-wrap: wrap; gap: .5rem; } + +.score-circle { + width: 8.25rem; + height: 8.25rem; + border-radius: 999px; + display: grid; + place-items: center; + text-align: center; + background: conic-gradient(var(--rs-primary), var(--rs-accent), var(--rs-secondary), var(--rs-primary)); + color: #fff; + box-shadow: var(--rs-shadow); +} +.score-circle strong { display: block; font-size: 2rem; line-height: 1; } +.score-circle span { display: block; font-size: .82rem; font-weight: 900; text-transform: uppercase; letter-spacing: .08em; } +.field-tile.missing { background: #fff7ed; border-color: rgba(249,115,22,.28); } +.field-tile.missing strong { color: #9a3412; } +.readiness-list, .missing-list { list-style: none; padding: 0; margin: 0; display: grid; gap: .8rem; } +.readiness-list li, .missing-list li { + display: flex; + justify-content: space-between; + gap: 1rem; + padding: .85rem 0; + border-bottom: 1px solid rgba(16,34,31,.08); +} +.readiness-list span { color: var(--rs-muted); font-weight: 800; } +.readiness-list strong { text-align: right; } +.missing-list li { display: block; color: #9a3412; font-weight: 900; } + +.site-footer { + padding: 2rem 0; + color: var(--rs-muted); + border-top: 1px solid rgba(16,34,31,.08); + background: rgba(255,253,247,.74); + font-weight: 700; +} + +@media (max-width: 767px) { + .section-pad { padding: 3.25rem 0; } + .live-card { transform: none; } + .hero-proof div { flex: 1 1 100%; } + .empty-state { padding: 2rem 1.2rem; } + .score-circle { width: 7rem; height: 7rem; } +} + + +.endpoint-box { + display: grid; + gap: .45rem; + border-radius: 1.1rem; + padding: 1rem; + background: #fffdf8; + border: 1px solid var(--rs-line); +} +.endpoint-box span { color: var(--rs-muted); font-size: .78rem; font-weight: 900; letter-spacing: .08em; text-transform: uppercase; } +.endpoint-box code, .endpoint-code code, .webhook-preview { color: var(--rs-ink); white-space: pre-wrap; word-break: break-word; } +.endpoint-code { + border-radius: 1.15rem; + padding: 1rem; + background: #10221f; + color: #ecfff9; + border: 1px solid rgba(255,255,255,.08); + overflow-x: auto; +} +.endpoint-code code { color: #ecfff9; font-size: .88rem; } +.webhook-preview { display: inline-block; max-width: 18rem; }