Changelog
Cosa c'è di nuovo.
Ogni miglioramento alla piattaforma, come si spedisce.
· SecurityService
Servire l'applicazione pubblica ipoteca, indirizzata dal prodotto
Tre endpoint non autenticati sotto i piedi. più il CRUD dietro le proprie licenze di stato dell'organizzazione. **L'organizzazione è nominata dal PATH, mai dall'host.** Un prestatore che ha non ha puntato un dominio a noi ha ancora bisogno di un'applicazione white-labelled per inviare I mutuatari a, e... E' quel collegamento. Risolvere l'inquilino dal nome host dipingerebbe ogni prestatore del genere branding della piattaforma e file ogni piombo sotto l'organizzazione della piattaforma — che è proprio ciò che accade a qualsiasi cosa prossi attraverso la condivisione `api.kamocrm.com` gateway, dove `X-Forwarded-Host` non sopravvive. `GET /{productId}` restituisce branding più le rivelazioni un prestatore già pubblica ai piedi delle proprie pagine — non il registro dell'organizzazione; C'era il diritto di rifiutarlo una volta. "Già" ****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************** ORGANIZZAZIONE, quindi un membro indovinato non può pubblicare la fotografia di qualcun altro e linea diretta. `POST /{productId}` è fail-CLOSED su Capcha (a differenza del sign-in, che rimane permissivo per un client mobile che non invia nessuno), tasso limitato per IP in Redis, richiede la casella di consenso e crea il suo vantaggio attraverso `LeadService.createLead` — che ricontrolla il proprietario del prodotto prima di timbrare e che ogni altro percorso di aspirazione sulla piattaforma già imbuti attraverso. Un prodotto il cui mercato non è RESIDENTIAL MORTGAGE o COMMERCIAL MORTGAGE risposte 404, identico a una cancellata. Tutti e tre sono nella base di ratchet con il ragionamento scritto. ** `OrgStateLicenseController`** è come un prestatore entra in quello che è in realtà autorizzato a fare, per Stato, gated su CONFIGURE SYSTEM e indirizzato nella query su ogni scrittura — una licenza id è un UUID in un URL, e un aggiornamento che risolve il riga prima di controllare l'organizzazione è una modifica della normativa di un altro inquilino divulgazione. Inoltre: `applicazioneBaseUrl` sulla risposta dei fornitori, così la schermata delle impostazioni può offrire un link copiabile per prodotto costruito su `verifiedRootDomain` — lo stesso predisporre i percorsi del percorso di segnale, deciso qui perché un frontend derivante dall'elenco di dominio porterebbe un operatore un nome host il cui DNS non ha mai è stato rivolto a noi. E `appsLogoOverlayColor` cotto nel tema config.json, che è l'unico percorso che il branding del sito applicazioni prende.
· kamo-shared-library
Registra se un mutuatario dice che questa è la loro prima casa
Un acquirente di prima volta è idoneo per i programmi che nessun altro può utilizzare, quindi un vantaggio che non porta la risposta costa all'ufficiale una chiamata a chiederlo. Il pubblico domanda modulo di domanda; non c'era posto per metterlo. Nullable e a tre valori deliberatamente. Null significa "non chiesto", che è ciò ogni piombo creato prima di questa colonna e ogni percorso di assunzione che non raccolgarlo dovrebbe leggere come — il default a falso direbbe a un ufficiale il Il mutuatario ha risposto no. Premesso, come tutto il resto su LeadClaimMortgage: nessuno ha controllato un titolo record, e il firewall HMDA su tale entità si applica invariato.
· kamo-apps
Ricostruire l'applicazione ipoteca come una forma reale, bianco-labelled
La pagina che sostituisce era un prototipo che indossava un URL di produzione. E' stata posta `http://localhost:8082`, quindi nessuna applicazione che abbia mai raccolto ha raggiunto nulla. Ha nominato "Mark Johnson con American Lending Group, NMLS 343298954" come ogni Agente di prestito del visitatore. Il suo piè di pagina ha rivendicato un Mortgage Residenziale della California Licenza di Lending Act, un ufficio al "123 Mortgage Lane", e una linea di reclami lettura "1-800-XXX-XXXX" — sotto il quale il logo dell'inquilino è accaduto a caricare. The il testo del consenso ha chiesto ai mutuatari di accettare i termini da "[ Nome del giocatore]". **Addressed by product, not by host.** `/loan/mortgage/{productId}`, with an opzionale `/{memberId}` per il link di riferimento di un funzionario di prestito. I nomi del prodotto l'organizzazione, quindi la pagina funziona identicamente su un prestatore `apps.theirdomain.com` e su `apps.kamocrm.com` — stesso logo, colori, favicon, in entrambi i casi. Questo è ciò che rende il white-labelling possibile per un prestatore che non ha configurato DNS, ed è per questo che nulla qui risolve un inquilino dal nome host: un proxy all'host API condiviso perde `X-Forwarded-Host`, e ogni tale prestatore avrebbe risolto alla piattaforma. Un prodotto il cui mercato non è un mercato ipotecario risponde 404, identico a un cancellato uno, e la pagina dice che l'applicazione non esiste. **Il piè di pagina è ora il record dell'organizzazione** — nome legale, NMLS con un Collegamento di accesso al consumatore, indirizzo reale e contatto, le licenze di stato che ha inserito, il suo marchio equo, e i suoi link politici pubblicati. Un valore che il prestatore ha non è entrato non rende affatto nulla. Un piè di pagina vuoto è un divario che qualcuno può riempire; il precedente era una falsa dichiarazione di regolamentazione che nessuno poteva rilevare. Disegno. Due colonne: una spina fissa colorata che porta il segno, il ufficiale e un continuo progresso ferroviario, contro carta. La colonna vertebrale è tutta idea — risponde "chi sto dando questo a" e "quanto più c'è" permanentemente, che sono le due ansie di questa forma. Froghe per dichiarazioni, Inter Tight per il lavoro, entrambi self-hosted così né l'immagine costruire o la prima vernice di un mutuatario dipende da Google. Figure tabulari ovunque un numero è digitato. Un momento orchestrato a carico; dopo, il movimento risponde solo ad un'azione, e `MotionConfig ridottaMotion="utente" rende che una vera preferenza piuttosto che un framer ignora. **Submission** va al punto di riferimento pubblico di SecurityService, chiuso con un Capcha ***, e atterra su una pagina di conferma che è un percorso reale — esso sopravvive ad un rinfresco e ad un link inoltrato. L'email di ricezione viene inviata sul lato server dallo stesso profilo divulgativo utilizzato dal piè di pagina, quindi i due non possono derivare. Verificato in un browser senza testa a 1440 e 390: cinque passi, validazione, errore afferma, la conferma, la pagina non trovata e ascia pulita (WCAG 2.1 AA) su tutti e quattro — un vero fallimento di contrasto trovato e fissato nella rotaia.
· kamo-login
Sigillare ogni schermo in un pannello arrotondato sotto un nastro d'angolo
Ogni percorso ora rende all'interno .kamo-pane: una scheda inset 15px dal browser bordo con un raggio di 14px, un'ombra morbida e troppo pieno: nascosto, quindi una pagina è propria il contenuto è clipped al rettangolo arrotondato. Il pannello porta anche un translateZ(0), che è ciò che lo rende il blocco contenente per `posizione: fisso` discendenti — senza di esso la pillola Powered-by e lo sfondo Capcha ancora a il viewport e sfuggire completamente alla clip, che è esattamente il caso è stato chiesto il ritaglio. Il nastro "Welcome Back!" è tre strati intorno a quel pannello, ordinato all'interno .kamo-shell: le code più scure dietro di esso, il pannello, poi la banda rossa davanti. La banda è ruotata di 45 gradi in senso orario sulla diagonale dell'angolo in alto a destra e clipped da una scatola ancorata 8px all'esterno di quell'angolo, in modo da sporge la parte superiore e bordi a destra esattamente 8px, tagliare il filo con il gap. Il taglio a filo non è una scorciatoia, è l'unico modo per colpire 8px. Un gruppo con le estremità quadrate perpendicolari a se stessa non possono sporgere un bordo da meno della sua proprio spessore su radice due — 24px a una banda di 34px leggibile, molto più di un 15px gap ha spazio per. Tagliando le estremità parallele ai bordi incrociati decouples lo strapiombo dallo spessore della band, e dà entrambi i bordi lo stesso 8px. Le code iniziano la larghezza di una band dall'angolo in modo che le loro estremità vicine finiscano sotto di esso, correre lungo ogni bordo attraverso il gap, e sono poi inghiottiti da il pannello. Solo --ribbon-over dello spessore di ogni coda sgombera mai il pannello, quindi la tacca di coda di rondine è tagliata dentro quella striscia; nulla più profondo non mostra mai. Ogni dimensione è una proprietà personalizzata su :root, e l'intero assemblaggio scale sotto i 640px. Ogni percorso si è dimensionato sul viewport, e così ha traboccato il nuovo pannello da 30px. Ogni radice ora riempie il pannello invece — 100% al posto di --app-dvh, 100dvh, 100vh e min-h-screen — e l'imbottitura di zona sicura si sposta fino alla fase, che è dove l'inizio è ora applicato. La tappa torna a 100 dvh per le rotte che non misurano visualViewport in --app-dvh stessi. Verificato in un browser a 1440x900 e 390x844: il pannello misura 15px da ogni bordo, la scatola di clip siede 8px fuori dall'angolo, entrambe le code atterrare dove la geometria prevede, e la pillola Powered-by è contenuta 16px all'interno del pannello piuttosto che il viewport.
· kamo-shared-library
Dare un prestatore da qualche parte per mettere la verità su se stessa
La pagina di applicazione dell'ipoteca pubblica ha composto il suo piede legale dal nulla — una licenza California, un numero NMLS inventato, un indirizzo di fantasia — e servito sotto il logo di ogni inquilino. Non c'era fonte di dati che potesse l'hanno resa vera. Questa è la fonte, più le due politiche affrontano la stessa il piè di pagina ha bisogno e non ha avuto nulla da leggere. **L'organizzazione** guadagna `privacyPolicyUrl`, `termsOfServiceUrl` e `appsLogoOverlay`, tutto nullable e tutto in `patchOrganization`````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````` che non è facoltativo: una colonna che il metodo non nomina è accettata con un 200 e silenziosamente mai memorizzato, e l'unico sintomo è un'impostazione che ritorna sul suo carico successivo. ** `OrgStateLicense` ** è il registro di licenza per Stato della società. Distinzione da `mlos.legal. StewardLicense`, che è per-MEMBER e risponde "potrebbe questo l'ufficiale lavora questo fascicolo" — nessuna licenza dell'ufficiale sta per la società. The entità, il suo repository (in `dao.repos`, che ogni servizio già scansiona; un nuovo pacchetto fallisce l'avvio contestuale in ogni servizio che non l'ha aggiunto) e `LenderDisclosureProfile`, che riduce un'organizzazione più le sue licenze attive alle frasi stampa piè di pagina. La sua regola di governo: un valore mancante produce NO LINE, mai un default plausibile. Un piè di pagina vuoto è un divario che qualcuno può riempimento; uno fabbricato non è rilevabile. ** `WebAddresses.normalize`** trasforma `www.example.com/privacy.html` in un URL assoluto, e rifiuta `javascript:`/`data:`. Il valore atterra in un `href` su pagine non autenticate, dove un ospite senza schemi si risolve come un percorso RELATIVE — presente, in stile, cliccabile e 404 per ogni visitatore. **'LOAN APPLICATION RECEIVED`** è la ricevuta del mutuatario, white-labelled da lo stesso profilo di divulgazione della pagina, quindi i due non possono derivare — nessuno legge la loro email di conferma, quindi una licenza rimossa dal sito web sarebbe altrimenti continuare a uscire per posta per mesi. I suoi blocchi sono pre-rendered in `LoanApplicationReceivedEmail` perché il renderer non ha condizionali, e La chiave di invio e' il DOTTED. costante — la divisione che ha spedito QUOTE SENT inaffidabile. 18 nuovi test, tra cui una affermazione che un'organizzazione che non è entrata la licenza non prevede alcuna licenza.
· kamo-internal
Pubblicare gli indirizzi politici, le licenze di stato reali e un link per prodotto
Tre cose che il sito pubblico di domanda di prestito non poteva essere detto in precedenza, e uno è stato detto male. **Le informazioni acquisiscono l'Informativa sulla privacy Indirizzo e Termini di servizio Indirizzo.** Entrambi facoltativi. Sia normalizzato ad un URL assoluto su blur e di nuovo su server, perché il valore atterra in un `href` su pagine che chiunque può aprire, e un bare host senza alcun schema si risolve lì come un percorso RELATIVE — il collegamento rimane presente, in stile e cliccabile, e 404s per ogni visitatore. `app/lib/webAddress.ts` rispecchia la libreria condivisa `WebAddresses.normalize` in modo che la scatola mostra ciò che sarà in realtà essere memorizzato piuttosto che sorprendere l'operatore dopo una ricarica. **Un riquadro di licenza di stato, in Informazioni aziendali.** La pagina di applicazione del mutuo spedito un codice duro California divulgazione — "Licensed by the Department of Supervisione aziendale sotto la California Residential Mortgage Lending Act" — servito sotto il logo di ogni inquilino, compresi gli inquilini non autorizzati mai in California. Questa è una falsa dichiarazione di regolamentazione fatta in un nome del cliente. Non c'era alcuna fonte di dati che avrebbe potuto renderlo vero, quindi questa è quella fonte: nome, numero, regolatore, linea di denunce e optional Esatta formulazione che stampa verbatim. Un'organizzazione che non entra niente stampe nulla; il pannello mostra la frase composta prima di salvarla, perché il alternativa alla lettura qui sta sentendo su di esso da un mutuatario. **Vendors & Products trasporta un link di applicazione copiabile per prodotto.** «https://apps. E' il momento di tornare indietro. app.kamocrm.com — e l'avviso sopra la tabella dice chiaramente che la piattaforma host non è un link minore, perché un operatore che lo assume mostra Kamo branding non lo invierà. Non lo fa: la pagina risolve il creditore dal prodotto id nel percorso. La base viene dal server (`verifiedRootDomain`, lo stesso predicato sign-in percorsi su), mai dal nome host del browser, quindi Nessuno è consegnato un indirizzo il cui DNS non è mai stato rivolto a noi. Solo i mercati i cui prodotti hanno effettivamente una pagina ottenere la colonna; gli altri avrebbero 404. `npm test` green: 447 file, 5525 test e tutte le quattordici guardie.
· kamo-internal
Avviare una sessione di coder dal menu Linux Desktop, a un modello e uno sforzo che si mantiene
L'icona ha già aperto un desktop remoto e un terminale remoto. La terza cosa la gente effettivamente andare a quella macchina per era raggiungibile solo aprendo Interazione Centro, passare a Sessioni e trovare la Nuova riga -- tre mosse fare la cosa per cui l'icona è. prima che sia una destinazione, quindi la fila dei genitori non lancia nulla e i suoi figli sono le risposte. Modello e Sffort portano il loro valore attuale nell'etichetta -- a membro non dovrebbe dover espandere una riga per imparare che cosa la loro sessione verrà eseguita come -- e espandersi in gruppi radio letti da AI MODELS e AI EFFORTS, quindi un modello aggiunto al vocabolario appare qui senza che nessuno ricordi che questo file esiste. The regola sotto Effort separa ciò che la sessione verrà eseguita come dalla riga che corre esso, lo stesso dispositivo che la lista di dimensioni utilizza. Le due impostazioni NON sono locali a questo menu. Sono in piedi del membro scelta, lettura e scrittura attraverso l'usoAiRunSettings -- lo stesso gancio il Invia ad AI modello scelto in uno dei tre è sullo schermo negli altri due prima del menu ha finito di chiudere, ed è ancora lì sul browser successivo. Si cavalca il membro UI mappa di preferenza, che è già letto una volta per carico pagina, quindi questo non costa gita rotonda. Tre cose dovevano cambiare sotto: * NavToolItem guadagna `keepOpen`. Ogni riga utilizzata per chiudere il menu, che è giusto per una fila che va da qualche parte e sbagliato per uno che regola un'impostazione: modello e lo sforzo sono due metà di una decisione con il lancio sotto di loro, quindi chiusura sul primo pick fatto scegliere sia una commissione a tre visite -- e collassò il gruppo, nascondendo il segno che si era appena trasferito. * OpencoderSession si unisce openTerminal in app/lib/terminal. L'interazione Centro inizia sessioni e deve, perché possiede una riga di partenza, una linea di errore e un elenco terminale per rinfrescare; ogni altra superficie ha una riga di menu e nient'altro. Non rifiuta mai -- una riga di menu non ha alcun posto per mettere un respinto promessa -- e consegna il rifiuto della macchina al brindisi, verbatim, il modo il pannello lo stampa. * La carta home launchpad rende i bambini. Ha disegnato solo il livello superiore di ogni sezione, che silenziosamente ha lasciato cadere ogni array `bambini` in strumentoSezioni: Remote Desktop, ogni argomento Impostazioni e ora questo era righe un membro potrebbe fare clic per sempre con nulla che accada. Non sono mai state file rotte -- sono domande le cui risposte non sono mai state sullo schermo. Ora li espande con il la regola di nav (una fila con una destinazione va là, una fila che è solo una il genitore si espande, una fila che è entrambi va là e si attiva sul suo chevron), quindi le due superfici che leggono una lista di sezione non possono più essere in disaccordo su cosa click significa. Il tooltip ha detto "Remote Desktop o Terminal" e ora nomina tutti e tre. Cancellato da tutti i 21 file locali nel dizionario nella stessa spinta, perché la modifica L'inglese sotto una chiave tradotta non cambia altro che l'inglese.
· kamo-internal
Finire il pannello dei calendari personali, e metterlo dove un membro può raggiungerlo
PersonalProviderSezione era impalcatura che nulla ha reso, e non avrebbe ha funzionato se aveva: ha letto GET /member/{id}, un percorso che non esisteva, quindi ha fallito all'aperto; ha offerto un pulsante Aggiungi per Google e Microsoft che POSTed a riga senza credenziali, producendo una connessione che non potrebbe mai sincronizzare; la sua due vie toggle inviato bidirezionaleSync dove la colonna è bidirezionale, quindi la commutato e nulla è stato memorizzato; e aveva un "Unify con l'organizzazione" Interruttore per un campo ContattoIntegrazione non ha, che il servizio ha abbandonato ogni scrittura. Ora elenca ciò che un membro ha collegato con il suo stato di sincronizzazione reale, collega Google e Microsoft attraverso il flusso OAuth, prende un URL e una password per iCloud e server personalizzati, e dice quando una connessione non contiene credenziali -- che gli interruttori da soli non possono, e che è la differenza tra "off" e "non può lavoro". L'interruttore unify è andato piuttosto che falsificato: non c'è posto per memorizzarlo, e un interruttore che silenziosamente persiste nulla è peggio di nessun interruttore. DavOAuthConnect guadagna un campo piuttosto che essere copiato. Tutto duro su di esso -- un richiamo su un'altra origine, un popup che non può segnalare il proprio risultato, il rileggere che è l'unico segnale affidabile -- è identico per un membro e per un'organizzazione, e una seconda copia andrebbe alla deriva esattamente sulle parti nessuno rilegge. Nell'ambito di applicazione del membro si aggiunge ?member=true e il server prende il membro dalla sessione, quindi il pannello non può rivolgersi a nessun altro. Vive nel cassetto delle preferenze del calendario. L'argomento delle impostazioni e-mail è gated su MANAGE EMAIL SETTINGS, quindi mettere i calendari personali ci sarebbe li ha nascosti da quasi tutti i loro calendari. Ultimo nel cassetto e dietro un divisore: ogni altro blocco c'è una preferenza il pannello possiede, mentre questo autorizza una terza parte e può fallire in modi su cui il membro deve agire. I percorsi chiave sono letterali e lo spazio dei nomi è in linea. check-i18n-keys risolve usoTraduzioni(' <literal>') e rinuncia all'usoTraduzioni(SOMETHING) -- e rinunciare significa saltare ogni chiave nel file, non solo uno, quindi un truffatore lì avrebbe lasciato tutti i trenta incontrollati. Raggiungere t attraverso una ref per tenerlo fuori le dipendenze del caricatore nasconde un sito di chiamata dalla guardia allo stesso modo, quindi loader imposta una bandiera e il render lo traduce.
· EmailService
Lasciate un membro collegare i loro calendari e contatti OWN
Il pannello dei calendari personali ha chiesto a GET di... che non è mai stato mappato -- così ha fallito su ogni aperto -- e per Google e Microsoft non ha avuto modo di autorizzare nulla affatto. Questa è la metà del servizio per farlo funzionare. Il membro viaggia nello stato OAuth, e questa è l'unica cosa che separa i due flussi. La registrazione dell'app, gli ambiti e l'URI redirect registrato sono tutti org-scoped e byte-identical per una connessione personale e un a livello di organizzazione, e il callback è senza sessione -- quindi senza un membro id in `state` una persona che autorizza il proprio account Google atterrerebbe la sovvenzione su la fila dell'ORGANIZZAZIONE e ridefinisce la sincronizzazione di ogni collega a una persona calendario. Absent significa ancora l'organizzazione, che è ciò che ogni stato scritto prima che sembri. /oauth/url, /oauth/status e DELETE /oauth take ?member=true, and the member proviene dalla SESSIONE piuttosto che dalla query -- un cliente che nomina un membro id sarebbe scegliere il cui calendario per connettersi. salvareMemberOAuth upserts da (membro, fornitore) piuttosto che inserire come salvareMemberIntegration lo fa. Insert è giusto per un server DAV digitato, dal momento che un persona può mantenere due, e sbagliato per una sovvenzione: c'è un account Google collegato alla volta, e riconsentare deve sostituirlo piuttosto che lasciare una seconda fila per i lavori di sincronizzazione per entrambi. GET /member/{memberId} ora esiste e rifiuta qualsiasi id ma il chiamante. Goccia l'id e la lettura della sessione invece sarebbe stato più semplice e sbagliato: chiedere per i calendari di qualcun altro sarebbe allora rispondere con il proprio. Non c'è diritto che lo apre -- una sovvenzione privata di Google non è un asset di organizzazione, quindi un l'amministratore non ha nemmeno letto su di esso.
· kamo-internal
Portare l'account utente sui diritti della piattaforma, non su quale org possiede
Autorizzato in una sessione concomitante su questa macchina e impegnato qui al richiesta dell'utente; registrato così la provenienza non è indovinata in seguito. L'account utente è il PERSON, e la modifica utilizzata per richiedere l'attuale organizzazione di possedere il conto. E' stato sbagliato due volte. Il proprietario di un account è una persona, non un'organizzazione, quindi la condizione ha risposto a una domanda amministratori ed è stato applicato a qualcuno che modifica i propri dati. Ed era un vicolo cieco in pratica: la piattaforma fornisce quasi il conto ogni membro inquilino — 77 di 141 iscritti il 2026-09-04 — e 37 di questi le persone non sono membri dell'organizzazione che possiede il proprio account, quindi nessun spazio di lavoro esisteva in cui potevano mai modificare il proprio nome preferito, sito web o link. Il cancello è ora il proprietario, un operatore dio-eleggibile, l'utente di sistema, o un titolare dei diritti della piattaforma che amministrano gli account utente (MANAGE USERS / SYSTEM USER — la scheda Utenti su /account). L'organizzazione di alto livello precondizione corre lungo piuttosto che essere controllato separatamente, perché un sessione all'interno di un inquilino non risolve affatto piattaforma giusta. I diritti degli inquilini rimangono deliberatamente assenti. MANAGE MEMBERS e MANAGE MEMBER SECURITY non raggiungere l'account utente: un amministratore inquilino può modificare il proprio i modelli di sicurezza dell'organizzazione, in modo che una holding inquilino o potrebbe concedersi la capacità di riscrivere l'identità ogni altra organizzazione che persona appartiene a vedere. Questo è lo stesso ragionamento che li tiene fuori dalla password. Con la condizione di possedere-org andato, il "il tuo account è gestito altrove" banner e il link cross-org che ha portato non hanno più nulla da spiegare, così loro e loro idraulici sono rimossi.
· EmailService
Dare l'albero della cartella di ogni fornitore la stessa forma prima che un cliente lo veda
Segnalare l'attributo SPECIAL-USE di una cartella non era sufficiente, perché solo un server invia uno se è configurato a — e il KamoMail Dovecot in produzione invia nessuno: la sua lista risponde al ruolo:null for Sent, Trash and Junk allo stesso modo. Quindi... Google mailbox ha ancora mostrato "[Gmail]" tra le proprie cartelle del membro con le loro reale Inviato sepolto dentro di esso, e un vuoto "Sent" pinned sopra puntando su una cartella Gmail non ha. FolderRoleMapper risolve ogni ruolo da, in ordine: l'attributo del server inviato, che è l'unica fonte che conosce un "Gesendet" localizzato è la cartella Sent; i percorsi che questo fornitore è noto per l'uso; e il nome di foglia della cartella, che è dove ogni server senza SPECIAL-USE finisce ed è esattamente ciò che il client faceva per sé. Un ruolo viene rivendicato una volta, quindi un membro "Sent" su Gmail non può prendere lo slot da [Gmail]/Sent Mail. Segna anche i contenitori un fornitore avvolge le sue cartelle in — [Gmail] e [Google Mail] — come nascosto. Sono ancora elencati, perché i loro figli nome loro come loro genitore, ma nessun cliente li mostra. Aggiungere un fornitore sta aggiungendo una riga alla mappa piuttosto che un altro ramo in un componente, e sia la casella di posta del membro e una condivisa passa attraverso di esso.
· kamo-internal
Fare Collegare tramite OAuth sulla scheda CalDAV/CardDAV realmente collegare
Il pulsante allerta(' Flusso OAuth non ancora implementato - configurare le credenziali manualmente tramite l'API'). Ora apre il vero Google Workspace / Microsoft 365 flusso di consenso e segnala cosa è tornato. Il confine della fiducia è la parte interessante. Il popup si completa sul callback registrato nella console del fornitore, che viene servita sull'host API -- a origine diversa da questa finestra -- quindi il suo postMessage di solito non riesce l'origine controllare e il risultato è imparato dal popup semplicemente chiudendo. Un segnale che non porta alcun risultato non deve mai essere letto come un successo, quindi ogni percorso finisce chiedendo il server che ha effettivamente memorizzato: un membro che chiude la schermata di consenso è non ha mostrato alcuna connessione. Il pannello incorpora anche OrgOAuthAppSection, così l'applicazione questo si collega attraverso è visibile e modificabile qui, ed è la registrazione SAME la scheda Impostazione Provider legge. Tre correzioni trovate durante il cablaggio, tutto in questa scheda: - Un loop di rendering. Il caricabatterie di stato dipendeva da `t`; usareTraduzioni() non promettere un'identità stabile tra i render, quindi il caricatore è stato ricreato, la sua effetto incendiato, ha regolato lo stato e di nuovo tondo. Qui non c'è niente che non trattiene callback now -- un problema viene portato sia come wording il servizio scelto o come Una delle quattro chiavi di nome. - La scheda si è espressa su ogni carico. GET /org risponde 204 per un org senza integrazione, e contact integrations è vuoto cluster-wide, quindi res.json() gettato sul corpo vuoto e ogni visita sollevata "Failed to load integration settings". - L'interruttore bidirezionale non ha mai riflettuto ciò che è stato memorizzato: si legge `bidirezionaleSync` dove la riga porta `bidirezionale`. Leggi entrambi, invia il una la colonna prende il nome. La barra di sincronizzazione accanto ha stampato il suo servizio enum raw (IDLE / SYNCING / ERROR) con a hard-coded "Ultima sincronizzazione:", e non ha mai mostrato il messaggio di errore della riga affatto -- quindi un chip ERROR ha detto a un membro qualcosa di sbagliato e non ha dato loro nulla per agire. Ora nomina gli stati e mostra la ragione, e Sync ora rilegge la riga E' appena scappato. La scheda rimane inglese hard-coded è tradotto anche. I percorsi chiave sono completati in ogni call site piuttosto che costruito da un prefisso condiviso: script/check-i18n-keys.mjs risolve un t letterale('a.b.c') e un modello la cui parte statica viene prima, ma non può vedere attraverso t(`${KEYS}.c`) -- verificato rompendo una chiave dietro tale prefisso e guardando il La guardia e' ancora pulita. Dietro una di quelle, tutte e 15 le nuove chiavi avrebbero spedito non custodito, e un typo rende come un percorso chiave crudo sullo schermo.
· EmailService
Collegare i calendari Google e Microsoft su OAuth, sull'app la casella di posta utilizza già
Il Connetto della scheda CalDAV/CardDAV tramite il pulsante OAuth ha sollevato un avviso dicendo che flusso non è stato implementato e configurare le credenziali tramite l'API. Ora corre una connessione reale, e il vincolo che ha plasmato è che un reindirizzamento OAuth URI vive in una console di THIRD PARTY: Google e Entra ciascuno tengono esattamente uno indirizzo per registrazione app, digitato da chiunque l'abbia registrato, quindi un secondo il flusso non può avere un secondo richiamo. Entrambi i flussi quindi atterrare sull'indirizzo già registrato e viene detto a parte un marcatore `flusso` dentro `stato` -- OAuthFlowKind, assente significa EMAIL così callback in volo continuare a completare dove L'hanno sempre fatto. Nessun cambio console, nessun nuovo percorso gateway, nessuna nuova sessione esenzione per un percorso pubblico. Ciò che il nuovo flusso condivide con il flusso di posta condivide deliberatamente: - La registrazione dell'app. GroupwareOAuthService chiede PlatformOAuthClientResolver la stessa domanda che il Provider Setup scheda chiede -- l'organizzazione OWN Google o Entra app se ha registrato uno lì, altrimenti app piattaforma di Kamo -- così le credenziali inserite una volta servono entrambi, e un org che non è stato detto così piuttosto che inviare a una schermata di consenso che rifiuta un client id vuoto. - L'URIDICA Reindirizzante, letta da... ribadito. Una copia, perché un secondo sarebbe un redirect uri mismatch in attesa per qualcuno di spostare un callback, ed è già coperto da OAuthRedirectUriTest contro la tabella OAuthCallbackPaths condivisa. Ciò che non condivide è la sovvenzione. Richiede Calendario e Contatti piuttosto che Gmail e l'Admin SDK, e memorizza i suoi gettoni sull'org ContattoIntegrazione piuttosto che la sua riga di e-mail provider, in modo da collegare i calendari non può disturbare una casella di posta funzionante. Sconti di lettura-scrittura, non .readonly: sincronizzazione a due vie è un interruttore il membro capovolge dopo il consenso, e gli scopi sono fissati al consenso. La connessione si dimostra prima di segnalare il successo -- legge l'account che ha concesso l'accesso e elenca un calendario. Una sovvenzione che non può leggere calendari, o che è tornato senza token di aggiornamento, è memorizzato e contrassegnato con cosa fare circa esso; entrambi sono altrimenti invisibili fino a quando una sincronizzazione corsa giorni dopo. Credenziali sono scritti nella forma i quattro rinfrescatori esistenti già letti da chiave di stringa (accessToken / rinfrescaToken / tokenExpiry come epoca millis / clientId / clientSecret / inquilino), quindi una connessione rimane viva senza ulteriori lavori, e GroupwareOAuthServiceTest spilla quei nomi: rinominando uno compila, distribuisce, collega, e smette di lavorare un'ora dopo. Accanto, tre correzioni nella stessa superficie: - GET /settings/integrations/org ha restituito l'entità, che ha messo l'org AES-encrypted credenziali blob in un corpo di risposta, una cache del browser e ogni registro proxy tra qui e il membro, su ogni carico di uno schermo che mai legge il campo. Entrambi gli endpoint org ora restituiscono una visione esplicita della riga, riferire solo WHETHER una credenziale è sul file. - applicareConfig leggere `bidirezionale` mentre ogni schermata delle impostazioni ha sempre inviato `bidirectionalSync`, quindi l'interruttore bidirezionale-sync non perse mai. Accetta entrambi. - ClearOrgCredentials, quindi Disconnect dimentica la sovvenzione senza resettare la opzioni di sincronizzazione che il membro ha scelto -- che deleteOrgIntegration avrebbe. La sincronizzazione dei calendari e dei contatti è invariata e ancora inedita: Google e Microsoft GroupwareProviders sono stubs delegare al DB locale, ProviderRegistry lega KamoGroupwareProvider qualsiasi tipo di fornitore, e I lavori di sincronizzazione di DaemonService solo urtano una sincroniaToken. Questo atterra le credenziali quelle avrà bisogno, e nulla ancora lo legge.
· kamo-shared-library
Ripeti il membro legge che fallisce prima dopo un DDL
Il carico membro-by-id è vicino alla prima dichiarazione una richiesta autenticata esegue, quindi un urto di conversione del catalogo lo colpisce prima che colpisca altro. Che cosa? è il motivo per cui l'ultima finestra è stato segnalato come "il widget del tabellone non può essere raggiunto": la query che in realtà ha fallito era un carico di identità noioso condiviso da quasi ogni endpoint, e qualsiasi caratteristica ha annunciato il proprio fallimento più forte Ho la colpa. Annotato qui piuttosto che al 96 membroRepository. trovareById siti di chiamata in Servizio di sicurezza, nessuno dei quali è un punto di riferimento e la maggior parte dei quali siedono all'interno I metodi @Transactional dove una retry non farebbe affatto nulla. Queste quattro letture sono nella biblioteca condivisa, quindi un posto copre la flotta. Il retry è significativo su esattamente questi metodi perché NON sono @Transactional: ogni chiamata di repository viene eseguita nella propria transazione implicita, quindi un secondo tentativo veramente ottiene un nuovo — lo stesso motivo per cui funziona OrgResolutionService. Aggiungere @Transactional a uno di loro in seguito senza muoversi la riprovazione verso l'esterno con esso avrebbe tranquillamente trasformare questi in no-ops. Legge solo. La ricerca di una lettura è sicura da ispezione; i percorsi di scrittura di questa classe sono lasciati soli. Il test afferma che l'annotazione è IN FORCE sulla classe reale attraverso un reale proxy, non che il retry helper funzioni. Ha fallito con il conflitto grezzo fino a quando il contesto ha registrato un creatore auto-proxy, che vale la pena conoscere: senza uno, il fagiolo esce senza pretese e ogni annotazione su di esso è inerte mentre perfettamente corretto. Non coperto: member.getOrganization(), che è una dereferenza LAZY risolta più tardi sotto la vista aperta, al di fuori di qualsiasi metodo un annotazione può avvolgere.
· MediaService
Invia l'avatar del mittente con il messaggio e la sua spinta
La notifica di chat ha portato `senderAvatar: "" con il commento "avatar risolto su frontend", e la spinta non ha portato avatar affatto. Un browser può risolvere uno — ha caricato la directory membro. Un telefono che solleva un galleggiante testa da un PUSH non può: l'applicazione potrebbe non essere in esecuzione, quindi non c'è nulla guardare qualcosa dentro, e la testa ha mostrato iniziali fino a quando il membro ha aperto il conversazione e minimizzato di nuovo. membroAvatarUrl è il gemello unico del richiedenteAvatarUrls e re-reads la fila avatar di id per lo stesso motivo: l'associazione è pigro e Avatar è una gerarchia JOINED, quindi il proxy off getAvatar() segnala il tipo BASE e ogni foto caricata sarebbe cercato il percorso vettoriale. Il miglior sforzo in tutto — un volto mancante si degrada alle iniziali, che è quello che un 404 sull'immagine avrebbe prodotto comunque, e non deve costare il messaggio.
· kamo-shared-library
Recuperare un metodo transazionale in una NUOVA transazione e fermare i diritti che non si aprono
Due correzioni che hanno senso solo insieme. @RetryOnDbConflict recupera un metodo OUTSIDE la sua transazione, quindi ogni tentativo inizia una nuova. Ripresa all'interno della transazione — che è quello che si ottiene se qualsiasi di questo è subtly sbagliato — non è un vantaggio più piccolo ma nessuno affatto: transazione che ha visto SQLState 40001 fallisce ogni dichiarazione successiva, quindi ogni tentativo in più funziona che non può commettere. Sembra anche che sia lavorare, che è la parte pericolosa. Il log riempie di ripetizioni, il metodo veramente si chiama tre volte, e l'errore alla fine è quello che avresti aveva comunque avuto. Quindi il test conta TRASPORTI STARTATI, non tentativi. Quei due numeri sono identico nel caso di lavoro e differisce in quello rotto, e nient'altro Li separa. Ha catturato tre veri difetti mentre questo era scritto: 1. Un semplice consulente @Component è invisibile al creatore auto-proxy che @EnableTransactionManagement installa, che considera solo i consulenti contrassegnati RUOLO INFRASTRUCTURE. Ogni rimpianto non ha fatto nulla. catena dell'intercettatore, quindi un secondo procedimento() ripreso oltre la transazione intercettatore e richiamato direttamente l'obiettivo, all'interno del fallito transazione. Ogni tentativo ora ottiene un invocatobleClone(). 3. Il consiglio non deve modificare il contratto di un metodo, quindi un'eccezione dichiarata controllata viene trasportato attraverso il confine di riprovazione e rilanciato invariato. Costruito in primavera normale AOP, non @Aspect, per evitare di mettere aspettojweaver in Sessanta immagini per un intercettore. Secondo: MemberRightsAppliedService non continua più quando il modello di sicurezza i modelli non possono essere letti. Registrare un avviso e portare su suoni come scelta attenta ed è il suo contrario — i modelli non sono solo una fonte di extra diritti, e un valore NUKE è come un modello dice "fuori per impostazione predefinita", quindi saltandoli lascia una giusta posizione che l'organizzazione intende sostenere. Il risultato fu poi perseverato. Un singhiozzo di database transitorio potrebbe quindi consegnare un membro accedere a nessuno concesso, con una linea WARN come unica traccia. Ora si lancia con la causa allegata, e i due punti di ingresso portano @RetryOnDbConflict — il causa realistica è un urto di conversione del catalogo e una riprovazione può accadere solo se il il fallimento è permesso raggiungerlo. 2757 test verdi.
· kamo-marketing
Dite ciò che il Piano Libero non addebita, accanto a quello che i pagati fanno
Ora che Starter e Business ogni portare una carica una volta sulla carta, un gratis scheda che non dice nulla su di esso invita esattamente l'inferenza sbagliata — il lettore chi ha appena letto "$99 per posto" due volte non ha motivo di assumere il livello sopra la piega è diversa. L'unico piano in cui "nessuna commissione di avvio" è ancora vero ora dice così, nella stessa posizione e la stessa voce dei suoi vicini, e risponde domanda carta di credito nello stesso respiro. Passato a mensilePrezzo === 0, che è libero da solo. L'impresa è -1 e negozia i propri termini, in modo da non affermare né una tassa né l'assenza di uno.
· SecurityService
Esporre che cosa un cambio di posto costerà prima che sia fatto
Aumentare il numero di posti di abbonamento ora solleva una fattura — una startup per-seat tassa segue la persona, quindi dieci noleggi su un piano $99 è $990 e un grande conto corre a quattro cifre. La console ha bisogno di qualcosa da chiedere prima di commettere un operatore a questo, e deve essere il server che risponde: un negoziato accordo può ignorare la tassa, e una cifra calcolata nel browser sarebbe un seconda implementazione di quella regola senza nulla che lo tenga onesto. Read-only, gated on MANAGE SUBSCRIPTIONS come il cambiamento che prevede.
· EmailService
Inviare la posta di un'organizzazione come proprio dominio quando il suo SPF dice che potremmo
L'indirizzo From è stato scelto chiedendo a chi ospita i MAILBOXES dell'organizzazione. Questa è una domanda diversa da quella che conta, ed è stato sbagliato in entrambi indicazioni subito. chinilaw.com funziona su Google Workspace e aveva aggiunto includere:spf.kamocrm.com a il suo SPF — autorizzando il nostro relè esplicitamente, per iscritto, in DNS — e ancora ottenuto "Ron Chini, PC" <NoReply@kamocrm.com> su ogni verifica e ripristino della password posta. Nel frattempo sei organizzazioni le cui SPF non ci hanno mai parlato inviavano come i loro domini, perché ospitiamo le loro caselle di posta: posta non autenticata rivendicare un dominio per cui non inviamo, ovvero i ricevitori di configurazione Spam-folder e rifiutare outright sotto una rigida politica DMARC. Quindi la regola è ora il record DNS: posseduto, e ci autorizza. Entrambi sono richiesto. La proprietà senza SPF è esattamente la posta non autenticata sopra; SPF senza proprietà permetterebbe a chiunque abbia aggiunto un meccanismo di includere un prestito dominio. Sconosciuto legge come no — un dominio che la spazzata non ha raggiunto ancora invia da l'indirizzo della piattaforma, perché indovinare male in quel modo costa branding mentre indovinare sbagliato l'altro modo costa la consegna, silenziosamente, sul messaggio che la maggior parte deve arrivare. Una selezione di righe alimenta sia l'indirizzo che la decisione. Leggere le bandiere un dominio durante l'invio come un altro autorizzerebbe qualcosa che nessuno ha controllato, e niente sembrerebbe sbagliato: il messaggio manda ancora. Un rifiuto duro al relè ora si riferisce una volta dall'indirizzo della piattaforma piuttosto che perdere la posta. Questa non è volutamente la protezione contro una dominio non autorizzato e non può essere: un errore SPF non è un errore di invio — il relay accetta il messaggio, il server ricevente lo accetta, ed è tranquillamente Spam-foldered o rimbalzato più tardi. Il pre-controllo è la protezione; questo è per la caso più stretto dove l'invio effettivamente ha lanciato.
· kamo-shared-library
Lascia che la console chieda quali costi di cambio del sedile prima di farlo
changeSeats ora solleva una fattura per la tassa di avvio su nuove teste, che gira un numero in una finestra di dialogo in una carica — dieci noleggi su un piano $99 è $990, e su un grande conto sono quattro cifre. Un operatore dovrebbe accettare questo piuttosto che impararlo dal cliente, quindi c'è qualcosa da chiedere. Solo per leggere, e risolto attraverso esattamente il codice che lo fattura. Computing la stessa figura nella console sarebbe una seconda implementazione di una regola che accordo negoziato può ignorare, e il numero nella finestra di dialogo deve essere il numero sulla fattura.
· SecurityService
Registra quali domini ci autorizzano, così la loro posta può uscire come loro
Transactional mail attualmente sceglie il suo Da indirizzo chiedendo chi ospita un l'organizzazione è MAILBOXES. Questa è la domanda sbagliata. Se possiamo inviare come chinilaw.com è deciso da chinilaw. il record di SPF e un'organizzazione Google Workspace può autorizzare il nostro relè perfettamente bene — chinilaw.com già ha, e ha ottenuto NoReply@kamocrm.com comunque. Quindi la risposta è letta da DNS e scritta, accanto alla proprietà e SSL bandiere che si trova accanto concettualmente. EmailService poi legge una colonna invece di Risolvere DNS sul percorso che consente di ripristinare la password. La sua spazzata, non quella SSL. Questo spazzare cammina domini SSL-CONFIRMED, e SPF non ha niente a che fare con i certificati: chinilaw. com'ssl confirmed è riaperto su un certificato di carriera mancante mentre il suo SPF è perfettamente buono. Attaccare questo avrebbe saltato l'unica organizzazione per cui è stato scritto. Una ricerca DNS fallita non scrive verdetto, solo un timestamp. I due modi di essere sbagliato qui non sono simmetriche: la registrazione di un dominio non autorizzato come autorizzato invia una mail non autenticata che riceve la cartella spam, e la password di org resetta l'arrivo con nulla da mostrare; la registrazione di un dominio autorizzato come costi non autorizzati solo marcando. Un risolutore blip non è una prova record SPF di chiunque, e non deve scambiare l'indirizzo di un'organizzazione fino al Il prossimo giro arriva. Il consiglio endpoint scrive anche il verdetto, quindi un proprietario che ha appena corretto il loro record vede che prende effetto sulla pagina che stanno guardando, piuttosto che un quarto d'ora dopo. Nulla legge ancora la bandiera; il cambio EmailService segue.
· kamo-shared-library
Registra se SPF di un dominio ci autorizza a inviare come esso
Due colonne accanto alla proprietà e alle bandiere SSL, rispondendo alla terza domanda DNS circa un dominio: possiamo inserire il suo nome sulla linea From della posta transazionale lo inviamo. Le colonne esistono già nel database — sono atterrate prima di questo, deliberatamente. Mappatura di una colonna che la tabella non ha rotture ogni query contro org domains in tutta la flotta, e il 500 si presenta in qualsiasi repository successivo legge un dominio piuttosto che in questo. Sia nulla che senza DEFAULT. NULL significa "mai controllato", che non è la stessa affermazione "controllata e non autorizzata": il primo è il silenzio, il seconda è la prova. Tenerli separati è ciò che consente a una ricerca DNS fallita di lasciare un verdetto esistente in piedi invece di demolire tranquillamente la posta di un'organizzazione per l'indirizzo della piattaforma fino alla prossima spazzata.
· kamo-internal
L'Hexhead di un terminale che ti vuole
Un membro con sei terminali minimizzati ha una domanda su ciascuno: è che uno Mi aspetta? La testa ora la risponde in un colore. lavoro verde — nulla è voluto da voi ambra ti aspetta, e non hai guardato da quando ha iniziato ad aspettare grigio in attesa, e l'hai visto Tre stati e non più. Un quarto dovrebbe essere letto piuttosto che guardare, e uno sguardo è l'intero punto di un colore. Il verde e l'ambra sono proprie dell'emulatore — lo stesso verde della lista dei terminali già utilizza per "questo guscio è attaccato" — così qualcuno che ha imparato uno ha Ho imparato l'altro. Il grigio non è volutamente un colore: un terminale che hai già visto dovrebbe recedere, e la strada per recede è smettere di essere colorato. La metà occupata / in attesa viene dalla macchina (un comando in primo piano della shell, un macchina può dire che un terminale è in attesa, ma solo questo browser sa se il da allora la persona l'ha guardato — e senza tale distinzione sei terminali isolati lucente per l'attenzione per sempre e il colore smette di significare qualsiasi cosa. Recensito significa visto SINCE che è STARTED WAITING, non visto mai. Un terminal ha aperto, letto e messo via, che poi è andato e ha fatto più lavoro e smesso di nuovo, ti chiede una seconda volta; una regola che ricorda solo "hai guardava questo" non poteva mai dirlo. Quindi il momento in cui un terminale smette di essere occupato è registrato e confrontato contro. La TRASFORMAZIONE è ciò che viene registrato, mai lo stato — un terminale già in attesa quando la pagina caricata non aveva, e timbrare "ora" segnerebbe ogni terminale sulla macchina non rivisto dopo ogni rinfrescati. Registrato sul filetto sessione, non la finestra dello strumento: ids finestra provengono da un contatore che si resetta su ogni carico di pagina, quindi una recensione registrata contro uno seguire qualsiasi terminale è accaduto per aprire prima dopo un aggiornamento. Sondaggi a 3s piuttosto che il pannello 10s — una lista è qualcosa che si va e leggere, ma un colore è qualcosa che si nota, e uno che richiede dieci secondi per andare ambra dopo una finitura costruttiva è un colore che si impara a non fidarsi. Solo mentre qualcosa è in realtà minimizzato, quindi un membro senza teste non paga nulla. Il colore è spinta solo quando ha cambiato, quindi una testa che è stata ambrata per dieci minuti non re-render ogni tre secondi. HexHeadTerminalStateSync siede accanto a HexHeadUnreadSync ed esiste per lo stesso ragione: è l'unica cosa che può vedere sia l'elenco delle finestre che una fonte di dati il motore hexhead non sa niente di. 13 guardie e 5381 test. check-i18n-keys rimane rosso su un'altra sessione lavoro di sottoscrizione, niente qui.
· SecurityService
Portare uno stato di attività del terminale alla console
TerminalSummary guadagna `stato`: occupato, o in attesa del suo membro. Leggi il testo trascrizione di sessione — nessuno dei quali è noto da qui, ed è per questo che viaggia piuttosto che derivare. Null da un agente troppo vecchio per inviarlo, che la console legge come attesa: un sbagliato "busy" nasconde che un terminale ha bisogno di qualcuno, un "svegliare" sbagliato tira solo l'occhio.
· kamo-internal
Dare una testa il proprio tooltip dell'app, posto da dove si siede
L'etichetta era la scatola nativa `title` del browser: unstyled, e mettere ovunque il altri 291 obiettivi hover in questo uso cromato, freccia e tutto. Una testa non è un normale obiettivo hover, quindi la punta degli strumenti è guidata piuttosto che attivato. I propri ascoltatori di MUI sono tutti spenti: l'elemento che li lega a è un involucro di trasformazione di dimensione zero nulla può saltare, e il suo tocco lunga pressione è lo stesso gesto che apre il menu contestuale della testa. Il palco possiede uno condiviso popper, lo apre dal puntatore di HexHead entrare / lasciare, e lo chiude su qualsiasi stampa — un'etichetta che pende da una testa oscillante sullo schermo non è un'etichetta. A 400ms traccia di etichette lampeggianti. L'ancoraggio è un elemento virtuale che legge i valori di movimento della testa su ogni chiamata, Non il nodo DOM. Le teste si muovono scrivendo la loro trasformazione direttamente al DOM con no React render in tra, quindi un ret misurato all'aperto va stallo il momento un il cluster rifluisce intorno a una finestra appena aperta; Popper è nudged per ri-correre quando questi valori cambiano. Il posto è deciso da dove la testa è, per postoTooltip: aperto lontano dal bordo orizzontale più vicino, e per una testa nel terzo esterno dello schermo allineare il tooltip è vicino bordo ad esso in modo da cresce verso l'interno invece di essere spinto indietro su schermo. Il flip di Popper e impedire Overflow rimanere sotto come rete di sicurezza, su il viewport con la stessa banda di mantenimento 8px che il menu di contesto utilizza — coprono che la geometria da sola non può, un titolo più ampio dello spazio che lascia il posizionamento. Verificato in Chrome, non solo in jsdom: otto posizioni testa (quattro angoli, il centro, un titolo di 90 caratteri nell'angolo, e due teste trascinate dal bordo) attraverso un 1200x800 e un 480x900 viewport, tutte le sedici etichette completamente all'interno margine; e una testa spostata 250x120px da solo valore di movimento, con l'etichetta rintracciarlo al pixel.
· kamo-marketing
Dichiarare la tassa di avvio, e smettere di rivendicare non c'è uno
Il sito ha detto "$0 spese di configurazione" tre volte nei metadati della pagina dei prezzi e elencato "Non è obbligatorio onboarding fee" come vantaggio su HubSpot. Tutto è ora falso: i piani pagati portano una volta $99 per posto. Una pagina di prezzi che omette un carica a quattro cifre su un ordine di 25 posti non è semplicemente incompleto, è la ragione qualcuno incontra il numero per la prima volta al checkout. Il confronto HubSpot sopravvive onestamente. Loro $1,500-$3,000 Professional onboarding recurs per Hub; il nostro è una volta per sedile e un ordine di grandezza più piccolo. Questo era sempre il vero punto — il vecchio proiettile appena fatto da rivendicare uno zero che non facciamo più pagare. La tassa è letta dal catalogo piuttosto che scritta nella copia, per lo stesso motivo per cui il soffitto del sedile è: è un numero le bollette del server su, e una frase affermando che avrebbe continuato a dichiarare il vecchio il giorno che cambia. L'aritmetica vive in plan-pricing.ts accanto al termine scala, specchio SetupFees sul server, e la guardia che esiste per catturare un numero sbagliato rendering perfettamente ora Controlla. Un'asimmetria deliberata al confine del catalogo: un modello assente di default PER SEAT qui, dove il server predefinito a FLAT. Questo sito rende esattamente uno catalogo e ogni piano pagato in esso è per-seat; l'API segnala l'importo senza modello fino a quando i servizi sono ricostruiti contro la biblioteca condivisa che ha introdotto Ecco. Predefinire FLAT in quella finestra stamparebbe "$99" contro un ordine davvero fatturato $2.475, e sottolineando una carica in pubblico è il valore di fallimento di default lontano da. Si crede sempre un FLAT esplicito. Il calcolatore trasporta la tassa sulla propria linea piuttosto che nel totale, che è una cifra mensile ricorrente che divide i perUser — sommandoli avrebbe bilancio un una volta carica dodici volte.
· kamo-internal
Mostrare la tassa di avvio prima che venga addebitato, e dove un accordo lo sovrascrive
calcolare-prezzo ha sempre restituito un setupFee e la procedura guidata stampato da nessuna parte, quindi una carica che corre a quattro cifre su un grande ordine raggiunto prima fattura del cliente senza mai apparire sulla citazione che hanno accettato. Si'. siede accanto al subtotale ora, sul passo in cui il conteggio del sedile è scelto. Il chip di pagamento ha letto "AUTO BILL ? Auto-Bill : Invoice", che ha etichettato un comped conto e un cavo sgomberato come una fattura eccezionale — i due afferma che la maggior parte del bisogno a parte dirlo. È una tabella ora, e sia WIRE che COMP sono selezionabili dove il metodo è cambiato, che non erano. Gli editor di piano guadagnano il modello accanto all'importo, perché $99 in setupFee è o $99 o $2,475 su un ordine di 25 posti e il numero da solo non può dire quale. Termini negoziati ottenere un pannello nel cassetto org della console della piattaforma, accanto al figure commerciali piuttosto che sepolte, dal momento che sono la ragione quelle figure non sono d'accordo con il listino prezzi. Un accordo decaduto rimane elencato ma dimmerso: deve non leggere come quello in vigore, e non deve svanire nemmeno, perché è ciò spiega una fattura già sollevata.
· InitializerService
Seed il $99 per-seat tassa di avvio su ogni livello pagato
Il caricatore ha superato null per setupFee su tutti e quattro i piani, quindi un seme appena ambiente prezzi onboarding a nulla mentre le spese di produzione $99 un sedile — il tipo di gap che si trova solo da qualcuno che prova un preventivo e crederlo. Il modello viene passato in ogni call site accanto alla quantità, perché la quantità solo non dice che cosa significa, e il tasso viene chiamato una volta così i tre pagati i livelli non possono allontanarsi. Gratis non tiene alcuna tassa affatto.
· BillingService
Bill la tassa di avvio su entrambi i percorsi a un piano, e record termini negoziati
Due percorsi raggiungono lo stesso piano e solo uno di loro stava per caricare il nuovo tassa di avvio per-seat. Una fattura d'ordine scritta dalle vendite tramite SubscriptionService, che non tocca mai Stripe; self-serve checkout va direttamente a un Checkout Sessione e non avrebbe accusato nessuno che si fosse iscritto. Lo stesso piano avrebbe costato diversi soldi a seconda di come è stato acquistato, quindi la tassa ottiene un prezzo Stripe non ricorrente e un elemento di linea, saltato quando un accordo rinuncia esso e saltato piuttosto che fatale quando non esiste ancora il prezzo — un prezzo mancante deve non interrompere la sottoscrizione di un cliente, e una tassa è recuperabile su una fattura in un modo un checkout abbandonato non lo è. Gli accordi API è piattaforma-admin solo e deliberatamente così: un accordo muove denaro a favore del cliente contro l'elenco pubblicato, che è di Kamo decisione e non del proprietario del cliente. Nulla cancella — ritirare disattivazioni e mantiene la fila, perché una fattura sollevata mentre era live deve rimanere Esplicabile. Due modi di registrare un errore sono rifiutati in modo definitivo: un FLAT accordo senza prezzi di importo a zero e legge come rinuncia a nessuno ha accettato, e un accordo modificato attraverso la pagina dell'organizzazione sbagliata ri-prezzi qualcuno che non faceva mai parte della conversazione. Il DDL è a mano, come gli script COMP accanto a esso erano: InitializerService non è distribuito, Hibernate non ristampa un CHECK su un tavolo esistente, e setup fee model è volutamente nullable quindi nessuna tassa di bordo del piano esistente è moltiplicato per un default nessuno ha scelto.
· kamo-shared-library
Caricare la tassa di avvio, per posto, e lasciare un cliente fuori di esso
La commissione di avvio è stata costruita a metà per tutto il tempo che il modello è esistito. SubscriptionPlan ha portato un setupFee, SubscriptionOrder ha portato un setupFee, OrderLineType e InvoiceLineType avevano entrambi SETUP FEE — e nessuno di essi mai ha raggiunto un conto. calcolarePrezzo aggiunto l'importo del piano al totale esattamente una volta, e generare fattura emesso linee PLAN e ADDON e fermato, quindi una tassa che è stato citato, concordato e memorizzato è stato addebitato a nessuno. KamoCRM ora addebita $99 per posto. Un singolo importo non può dire che: $99 in setup fee è $99 o $2,475 su un ordine di 25 posti, e niente sulla riga li distingueva. Quindi il significato è indicato accanto alla quantità, con FLAT come il default — ogni piano che la piattaforma ospita per un terzo è stato autore di carica singola e non deve essere retroattivamente moltiplicato. Tre chiamanti hanno bisogno della risposta e uno di loro è nuovo. Una tassa di soggiorno segue il persona, così crescendo la squadra deve di nuovo sulla differenza; senza che un L'account a 5 posti potrebbe raggiungere 500 dopo aver pagato l'installazione su cinque. cambiamentoSeats now solleva la propria fattura di una linea per le nuove teste, piuttosto che seppellire una carica una volta all'interno di un rinnovo che può essere undici mesi di distanza. Harmony Home Prestiti concordati un appartamento $2,000 che copre ogni posto che potranno mai aggiungere, prima di firmare. Le vendite hanno avuto una promozione (un codice, aperto a chiunque lo generi, espresso in percentuale) e totali di ordine a mano, e non ritiene che: una promozione non ha scopo per conto, e un ordine modificato è un fatto su uno ordine, dimenticato la prossima volta che il conto è valutato. OrgBilling Accordo lo registra contro l'organizzazione invece, dove i prezzi lo legge ogni volta. "$2,000 invece di $99 un posto" e "$2,000 e mai più" sono diversi promesse, così copreFutureSeats è chiesto da solo piuttosto che deferito — un il cliente che ha acquistato il primo sarebbe fatturato correttamente oggi e ingiustamente il mese che assumono. WIRE si unisce ai metodi di pagamento perché i soldi di Harmony erano già sgomberati e Non c'era posto per dirlo. Non è INVOICE (niente è eccezionale, dunning non deve inseguirlo) e non enfaticamente COMP, che è quello che sono stati portati come per una quarantina di giorni mentre il cavo si sedeva in banca — un conto lettura come gratuito in ogni rapporto che conta ricavi.

Il tuo universo ti sta aspettando.
Aggiungi i prodotti quando sei pronto: nessuna migrazione, nessun nuovo login, nessuna richiesta di pagamento.
Gratis per sempre — nessuna carta di credito richiesta