- Zainstalować Pulsar
- Zainstalować nodejs (npm)
- Pobrać ftp-remote-edit-master :
https://github.com/h3imdall/ftp-remote-edit/archive/refs/heads/master.zip
https://github.com/h3imdall/ftp-remote-edit/archive/refs/heads/master.zip
ALTER TABLE brands ADD `uuid` UUID DEFAULT UUID();
Faktura (Invoices) nie ma pól netto, brutto, total_netto, total_brutto. Dane o kosztach znajduą się w InvoicePositions.netto i InvoicePositions.brutto
PHP nie ma klienta AI – zapytania poprzez curl
Klucz API generujemy w Claude Console i przechowujemy zaszyfrowany w bazie danych
ALTER TABLE brands ADD anthropic_api_key_enc text DEFAULT NULL COMMENT "anthropic_api_key_enc";
.env – klucz szyfrujący (32 bajty = 256 bitów):
# php -r "echo base64_encode(random_bytes(32));"
// Szyfrowanie kluczy API i innych wrażliwych danych 'Encryption' => [ 'key' => 'meDX61dddsfasrfLMUwTxdfsdadfad=', ],
Serwis szyfrujący
// src/Service/EncryptionService.php namespace App\Service; use Cake\Core\Configure; use RuntimeException; class EncryptionService { private string $key; private string $cipher = 'aes-256-gcm'; public function __construct() { $raw = base64_decode(Configure::read('Encryption.key', '')); if (strlen($raw) !== 32) { throw new RuntimeException('APP_ENCRYPTION_KEY musi mieć 32 bajty (base64).'); } $this->key = $raw; } // Szyfruje tekst np. API KEY i koduje w Base64 public function encrypt(string $plaintext): string { $ivLen = openssl_cipher_iv_length($this->cipher); $iv = random_bytes($ivLen); $tag = ''; $encrypted = openssl_encrypt($plaintext, $this->cipher, $this->key, 0, $iv, $tag); // Zapisujemy IV + tag + ciphertext razem (base64) return base64_encode($iv . $tag . $encrypted); } // Odszyfrowuje zawartość np. zaszyfrowany API KEY w formie Base64 public function decrypt(string $stored): string { $decoded = base64_decode($stored); $ivLen = openssl_cipher_iv_length($this->cipher); $tagLen = 16; // GCM tag zawsze 16 bajtów $iv = substr($decoded, 0, $ivLen); $tag = substr($decoded, $ivLen, $tagLen); $encrypted = substr($decoded, $ivLen + $tagLen); $plaintext = openssl_decrypt($encrypted, $this->cipher, $this->key, 0, $iv, $tag); if ($plaintext === false) { throw new RuntimeException('Odszyfrowanie nie powiodło się — zły klucz lub dane uszkodzone.'); } return $plaintext; } }
src/Service/AnthropicService.php
namespace App\Service; use RuntimeException; use App\Model\Entity\Brand; use Cake\Core\Configure; use App\Service\EncryptionService; class AnthropicService { private string $apiKey; private string $apiUrl = 'https://api.anthropic.com/v1/messages'; private string $model = 'claude-sonnet-4-20250514'; private int $maxTokens = 1024; private int $maxHistory = 20; // max par wiadomości, potem przycinamy public function __construct(Brand $brand) { // Klucz API marki (zaszyfrowany z bazy danych) if (!empty($brand->anthropic_api_key_enc)) { $enc = new EncryptionService(); $this->apiKey = $enc->decrypt($brand->anthropic_api_key_enc); } // Klucz API globalny (jeśli marka nie ma własnego) else { $this->apiKey = Configure::read('Anthropic.apiKey', ''); } if (empty($this->apiKey)) { throw new RuntimeException("Brak klucza Anthropic dla marki {$brand->name}."); } } // construct /** * Wysyła wiadomość i zwraca odpowiedź. * Historia jest przekazywana z zewnątrz (sesja lub baza). */ public function chat(string $userMessage, array $history = [], string $systemPrompt = ''): array { // Dodajemy nową wiadomość użytkownika do historii $messages = $history; $messages[] = ['role' => 'user', 'content' => $userMessage]; // Przycinamy historię żeby nie przekroczyć limitu tokenów if (count($messages) > $this->maxHistory * 2) { $messages = array_slice($messages, -($this->maxHistory * 2)); } $payload = [ 'model' => $this->model, 'max_tokens' => $this->maxTokens, 'messages' => $messages, ]; if ($systemPrompt !== '') { $payload['system'] = $systemPrompt; } $ch = curl_init($this->apiUrl); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'x-api-key: ' . $this->apiKey, 'anthropic-version: 2023-06-01', 'content-type: application/json', ], CURLOPT_POSTFIELDS => json_encode($payload), ]); $body = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $response = json_decode($body, true); if ($httpCode !== 200) { $error = $response['error']['message'] ?? 'Nieznany błąd API'; throw new RuntimeException("Błąd Anthropic API ({$httpCode}): {$error}"); } $assistantMessage = $response['content'][0]['text']; // Zwracamy odpowiedź + zaktualizowaną historię $updatedHistory = $messages; $updatedHistory[] = ['role' => 'assistant', 'content' => $assistantMessage]; return [ 'message' => $assistantMessage, 'history' => $updatedHistory, 'usage' => $response['usage'] ?? [], ]; } // chat } // class AnthropicService
Użycie w kontrolerze
// src/Controller/ChatController.php
namespace App\Controller;
use App\Service\AnthropicService;
class ChatController extends AppController
{
public function index(): void
{
$anthropic = new AnthropicService();
$answer = $anthropic->ask(
'Jakie są zalety CakePHP 5?',
'Jesteś ekspertem PHP. Odpowiadaj zwięźle po polsku.'
);
$this->set(compact('answer'));
}
}
Struktura tabeli
Kopiujemy strukturę katalogów i plików ze zdalnego serwera na lokalny
Logowanie do konta Claude Pro https://claude.ai/
Instalacja globalna dla użytkownika (nie root)
Pliki poza aplikacją:
# mkdir -p /var/uploads/my_app # chown www-data:www-data /var/uploads/my_app # chmod 750 /var/uploads/my_app
https://github.com/FriendsOfCake/bootstrap-ui
# cd /var/www/my_project # cp -r vendor/cakephp/bake/templates/bake/ templates/bake/ - od tej pory do generowania modułu (bin/cake bake ..) będzie użyty lokalny szablon
# usermod -a -G www-data <username> // Weryfikacja # groups <username> <username>: username git www-data
$next_nb = str_pad((string) $next, 2, '0', STR_PAD_LEFT);
Lista ikon: https://icons.getbootstrap.com/
Najlepsza metoda – instalacja npm i kopiowanie do właściwych katalogów Czytaj dalej Bootstrap Icons
W szablonie:
<?php $user_id = $this->getRequest()->getAttribute('identity')->getIdentifier(); ?>
W kontrolerze:
$user_id = $this->Authentication->getIdentity()->getIdentifier();
$role = $this->Authentication->getIdentity()->get('role'); $role == 'admin' || 'user'
Wyłączenie z dostępu po zalogowaniu
use Cake\Event\EventInterface; public function beforeFilter(EventInterface $event): void { parent::beforeFilter($event); if(!$this->is_admin) { $this->Flash->error(__('Nie ma takiej strony')); $event->setResult($this->redirect('/')); return; } $this->Authentication->addUnauthenticatedActions(['edit', 'changePassword' ]); }
<!-- CSRF Token jako ukryte pole (fallback) --> <input type="hidden" name="_csrfToken" id="csrf-token-field" value="<?= $this->request->getAttribute('csrfToken') ?>">
let url = new URL(location.href); $("#proj-tabs").find(".nav-link").on('click', function() { var tab_name = $(this).attr('data-bs-target').replace("#", ""); url.searchParams.set("s_tab", tab_name); history.replaceState(null, null, url.href); });
Między serwerem developerskim -> repozytorium -> serwerem produkcyjnym
Instaluje główne repozytorium git na debianie:
# apt install git // dedykowany użytkownik git # adduser --system --shell /bin/bash --group --home /home/git git
Problem polega na tym, że CakePHP owija pole w <div class="input"> który nie ma width: 100% w kontekście Bootstrap input-group. Rozwiązanie — użyj opcji 'templates' żeby usunąć ten wrapper:
Czytaj dalej CakePHP Input Group z Bootstrap’em – pole input za wąskie
Usuwa żółte tła:
CREATE TABLE trs_new LIKE trs;
https://github.com/johnzuk/GusApi
https://api.stat.gov.pl/Home/RegonApi
validateNip($nip) - Sprawdza poprawność - Invoice::validateNip('1234567890') → true normalizeNip($nip) - Usuwa myślniki, spacje - Invoice::normalizeNip('123-456-78-90') → '1234567890' formatNip($nip) - Formatuje XXX-XXX-XX-XX - Invoice::formatNip('1234567890') → '123-456-78-90' generateRandomNip() - Generuje losowy - NIP - Invoice::generateRandomNip() → '857-234-19-23' getTestNip() - Stały NIP testowy - Invoice::getTestNip() → '123-456-78-16' isForeignNip($nip) - Czy NIP zagraniczny - Invoice::isForeignNip('9912345678') → true getNipInfo($nip) - Debug info Zwraca tablicę z detailami seller_nip_formatted - Virtual field - $invoice->seller_nip_formatted → '123-456-78-90' buyer_nip_formatted - Virtual field - $invoice->buyer_nip_formatted → '123-456-78-90'
cd /var/www/biznes
Assety zainstalowane za pomocą npm i skopiowane do webroot/
# cd /var/www/biznes # mkdir webroot/css/vendor # mkdir webroot/js/vendor